mirror of
https://github.com/facundoolano/jorge.git
synced 2024-12-25 21:58:28 +01:00
Add drafts support (#17)
* add src and target ext as template methods * add include drafts config * default to draft on jorge post * skip drafts from site indexes and rendering * fix stat usage for file detection * add site.Build test * test build site with and without drafts * add templ IsPost helper * document drafts in readme * document drafts in tutorial
This commit is contained in:
parent
e1280f07d5
commit
6f27afacf7
8 changed files with 240 additions and 40 deletions
|
@ -66,6 +66,7 @@ date: 2024-02-21 13:39:59
|
|||
layout: post
|
||||
lang: en
|
||||
tags: []
|
||||
draft: true
|
||||
---
|
||||
#+OPTIONS: toc:nil num:nil
|
||||
#+LANGUAGE: en
|
||||
|
@ -83,6 +84,12 @@ this is my *first* post.
|
|||
EOF
|
||||
```
|
||||
|
||||
Posts created with `jorge post` are drafts by default. Remove the `draft: true` to mark it ready for publication:
|
||||
|
||||
``` bash
|
||||
$ sed -i '' '/^draft: true$/d' src/blog/my-first-post.org
|
||||
```
|
||||
|
||||
Finally, you can render a minified version of your site with `jorge build`:
|
||||
|
||||
```
|
||||
|
|
|
@ -19,6 +19,7 @@ date: %s
|
|||
layout: post
|
||||
lang: %s
|
||||
tags: []
|
||||
draft: true
|
||||
---
|
||||
`
|
||||
|
||||
|
@ -56,7 +57,7 @@ func (cmd *Post) Run(ctx *kong.Context) error {
|
|||
}
|
||||
|
||||
// if file already exists, prompt user for a different one
|
||||
if _, err := os.Stat(path); os.IsExist(err) {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
fmt.Printf("%s already exists\n", path)
|
||||
filename = Prompt("filename")
|
||||
path = filepath.Join(config.SrcDir, filename)
|
||||
|
|
|
@ -31,9 +31,10 @@ type Config struct {
|
|||
Lang string
|
||||
HighlightTheme string
|
||||
|
||||
Minify bool
|
||||
LiveReload bool
|
||||
LinkStatic bool
|
||||
Minify bool
|
||||
LiveReload bool
|
||||
LinkStatic bool
|
||||
IncludeDrafts bool
|
||||
|
||||
ServerHost string
|
||||
ServerPort int
|
||||
|
@ -61,6 +62,7 @@ func Load(rootDir string) (*Config, error) {
|
|||
Minify: true,
|
||||
LiveReload: false,
|
||||
LinkStatic: false,
|
||||
IncludeDrafts: false,
|
||||
pageDefaults: map[string]interface{}{},
|
||||
}
|
||||
|
||||
|
@ -113,6 +115,7 @@ func LoadDev(rootDir string, host string, port int, reload bool) (*Config, error
|
|||
config.LiveReload = reload
|
||||
config.Minify = false
|
||||
config.LinkStatic = true
|
||||
config.IncludeDrafts = true
|
||||
config.SiteUrl = fmt.Sprintf("http://%s:%d", config.ServerHost, config.ServerPort)
|
||||
|
||||
return config, nil
|
||||
|
|
|
@ -13,6 +13,7 @@ So far you've seen how to [[file:jorge-init][start a project]], [[file:jorge-ser
|
|||
|
||||
#+begin_src console
|
||||
$ jorge build
|
||||
skipping draft target/blog/my-own-blog-post.org
|
||||
wrote target/2024-02-23-another-kind-of-post.html
|
||||
wrote target/blog/my-own-blog-post.html
|
||||
wrote target/blog/goodbye-markdown.html
|
||||
|
@ -26,6 +27,7 @@ wrote target/blog/index.html
|
|||
|
||||
Just like ~jorge serve~ did before, ~jorge build~ scans your ~src/~ directory and renders its files into ~target/~, but with a few differences:
|
||||
|
||||
- Templates flagged as drafts in their front matter are excluded.
|
||||
- Static files are copied over to ~target/~ instead of just linked.
|
||||
- The ~url~ from your ~config.yml~ is used as the root when rendering absolute urls (instead of the ~http://localhost:4001~ used when serving locally).
|
||||
- The HTML, XML, CSS and JavaScript files are minified.
|
||||
|
|
|
@ -44,6 +44,7 @@ date: 2024-02-23 11:45:30
|
|||
layout: post
|
||||
lang: en
|
||||
tags: []
|
||||
draft: true
|
||||
---
|
||||
#+OPTIONS: toc:nil num:nil
|
||||
#+LANGUAGE: en
|
||||
|
@ -51,13 +52,14 @@ tags: []
|
|||
|
||||
Let's look at what the command did for us:
|
||||
{% raw %}
|
||||
| ~src/blog/my-own-blog-post.org~ | The filename, a URL-friendly version of the post title (a "slug"), such that the post will be served at ~/blog/my-own-blog-post~ |
|
||||
| ~title: My own blog post~ | The title we passed to jorge. This will be available to templates as ~{{page.title}}~ and will be used by the default post layout to render the header of the page. |
|
||||
| ~date: 2024-02-23 11:45:30~ | The date this post was created. It will affect the position it shows up in in ~{{site.posts}}~ |
|
||||
| ~layout: post~ | The rendered HTML of this template will be embedded as the ~{{contents}}~ of the layout defined in ~layouts/post.html~. |
|
||||
| ~lang: en~ | The language code for the post. This is used by some of the default templates, for instance, to determine how to hyphenate the post content. |
|
||||
| ~tags: []~ | The post tags, initially empty. The keywords in this list will determine which keys of the ~{{site.tags}}~ map this post will be associated with.
|
||||
| ~#+OPTIONS: toc:nil num:nil~, ~#+LANGUAGE: en~ | Some default org mode options, to skip the table of contents and define the post language. |
|
||||
| ~src/blog/my-own-blog-post.org~ | The filename, a URL-friendly version of the post title (a "slug"), such that the post will be served at ~/blog/my-own-blog-post~ |
|
||||
| ~title: My own blog post~ | The title we passed to jorge. This will be available to templates as ~{{page.title}}~ and will be used by the default post layout to render the header of the page. |
|
||||
| ~date: 2024-02-23 11:45:30~ | The date this post was created. It will affect the position it shows up in in ~{{site.posts}}~ |
|
||||
| ~layout: post~ | The rendered HTML of this template will be embedded as the ~{{contents}}~ of the layout defined in ~layouts/post.html~. |
|
||||
| ~lang: en~ | The language code for the post. This is used by some of the default templates, for instance, to determine how to hyphenate the post content. |
|
||||
| ~tags: []~ | The post tags, initially empty. The keywords in this list will determine which keys of the ~{{site.tags}}~ map this post will be associated with. |
|
||||
| ~draft: true~ | By default, posts created with ~jorge post~ are marked as drafts. Drafts are included in the site served locally but excluded from the production build. Remove this flag once your post is ready.
|
||||
| ~#+OPTIONS: toc:nil num:nil~, ~#+LANGUAGE: en~ | Some default org mode options, to skip the table of contents and define the post language. |
|
||||
{% endraw %}
|
||||
|
||||
With ~jorge serve~ running, you can start filling in some content on this new post and see it show up in the browser at http://localhost:4001/blog/my-own-blog-post.
|
||||
|
|
|
@ -99,8 +99,13 @@ func Parse(engine *Engine, path string) (*Template, error) {
|
|||
return &templ, nil
|
||||
}
|
||||
|
||||
// Return the extension of this template's source file.
|
||||
func (templ Template) SrcExt() string {
|
||||
return filepath.Ext(templ.SrcPath)
|
||||
}
|
||||
|
||||
// Return the extension for the output format of this template
|
||||
func (templ Template) Ext() string {
|
||||
func (templ Template) TargetExt() string {
|
||||
ext := filepath.Ext(templ.SrcPath)
|
||||
if ext == ".org" || ext == ".md" {
|
||||
return ".html"
|
||||
|
@ -108,6 +113,18 @@ func (templ Template) Ext() string {
|
|||
return ext
|
||||
}
|
||||
|
||||
func (templ Template) IsDraft() bool {
|
||||
if draft, ok := templ.Metadata["draft"]; ok {
|
||||
return draft.(bool)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (templ Template) IsPost() bool {
|
||||
_, ok := templ.Metadata["date"]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Renders the liquid template with the given context as bindings.
|
||||
// If the template source is org or md, convert them to html after the
|
||||
// liquid rendering.
|
||||
|
@ -118,9 +135,7 @@ func (templ Template) Render(context map[string]interface{}, hlTheme string) ([]
|
|||
return nil, err
|
||||
}
|
||||
|
||||
ext := filepath.Ext(templ.SrcPath)
|
||||
|
||||
if ext == ".org" {
|
||||
if templ.SrcExt() == ".org" {
|
||||
// org-mode rendering
|
||||
doc := org.New().Parse(bytes.NewReader(content), templ.SrcPath)
|
||||
htmlWriter := org.NewHTMLWriter()
|
||||
|
@ -134,7 +149,7 @@ func (templ Template) Render(context map[string]interface{}, hlTheme string) ([]
|
|||
return nil, err
|
||||
}
|
||||
content = []byte(contentStr)
|
||||
} else if ext == ".md" {
|
||||
} else if templ.SrcExt() == ".md" {
|
||||
// markdown rendering
|
||||
var buf bytes.Buffer
|
||||
md := goldmark.New(goldmark.WithExtensions(
|
||||
|
|
58
site/site.go
58
site/site.go
|
@ -120,7 +120,7 @@ func (site *Site) loadDataFiles() error {
|
|||
}
|
||||
|
||||
func (site *Site) loadTemplates() error {
|
||||
if _, err := os.Stat(site.Config.SrcDir); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(site.Config.SrcDir); err != nil {
|
||||
return fmt.Errorf("missing src directory")
|
||||
}
|
||||
|
||||
|
@ -135,37 +135,42 @@ func (site *Site) loadTemplates() error {
|
|||
// set site related (?) metadata. Not sure if this should go elsewhere
|
||||
relPath, _ := filepath.Rel(site.Config.SrcDir, path)
|
||||
srcPath, _ := filepath.Rel(site.Config.RootDir, path)
|
||||
relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath)) + templ.Ext()
|
||||
relPath = strings.TrimSuffix(relPath, filepath.Ext(relPath)) + templ.TargetExt()
|
||||
templ.Metadata["src_path"] = srcPath
|
||||
templ.Metadata["path"] = relPath
|
||||
templ.Metadata["url"] = "/" + strings.TrimSuffix(strings.TrimSuffix(relPath, "index.html"), ".html")
|
||||
templ.Metadata["dir"] = "/" + filepath.Dir(relPath)
|
||||
|
||||
// posts are templates that can be chronologically sorted --that have a date.
|
||||
// the rest are pages.
|
||||
if _, ok := templ.Metadata["date"]; ok {
|
||||
// if drafts are disabled, exclude from posts, page and tags indexes, but not from site.templates
|
||||
// we want to explicitly exclude the template from the target, rather than treating it as a non template file
|
||||
if !templ.IsDraft() || site.Config.IncludeDrafts {
|
||||
// posts are templates that can be chronologically sorted --that have a date.
|
||||
// the rest are pages.
|
||||
if templ.IsPost() {
|
||||
|
||||
// NOTE: getting the excerpt if not set at the front matter requires rendering the template
|
||||
// which could be too onerous for this stage. Consider postponing setting this and/or caching the
|
||||
// template render result
|
||||
templ.Metadata["excerpt"] = getExcerpt(templ)
|
||||
site.posts = append(site.posts, templ.Metadata)
|
||||
// NOTE: getting the excerpt if not set at the front matter requires rendering the template
|
||||
// which could be too onerous for this stage. Consider postponing setting this and/or caching the
|
||||
// template render result
|
||||
templ.Metadata["excerpt"] = getExcerpt(templ)
|
||||
site.posts = append(site.posts, templ.Metadata)
|
||||
|
||||
// also add to tags index
|
||||
if tags, ok := templ.Metadata["tags"]; ok {
|
||||
for _, tag := range tags.([]interface{}) {
|
||||
tag := tag.(string)
|
||||
site.tags[tag] = append(site.tags[tag], templ.Metadata)
|
||||
// also add to tags index
|
||||
if tags, ok := templ.Metadata["tags"]; ok {
|
||||
for _, tag := range tags.([]interface{}) {
|
||||
tag := tag.(string)
|
||||
site.tags[tag] = append(site.tags[tag], templ.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// the index pages should be skipped from the page directory
|
||||
filename := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
|
||||
if filename != "index" {
|
||||
site.pages = append(site.pages, templ.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// the index pages should be skipped from the page directory
|
||||
filename := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
|
||||
if filename != "index" {
|
||||
site.pages = append(site.pages, templ.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
site.templates[path] = templ
|
||||
}
|
||||
return nil
|
||||
|
@ -256,12 +261,17 @@ func (site *Site) buildFile(path string) error {
|
|||
defer srcFile.Close()
|
||||
contentReader = srcFile
|
||||
} else {
|
||||
if templ.IsDraft() && !site.Config.IncludeDrafts {
|
||||
fmt.Println("skipping draft", targetPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
content, err := site.render(templ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetPath = strings.TrimSuffix(targetPath, filepath.Ext(targetPath)) + templ.Ext()
|
||||
targetPath = strings.TrimSuffix(targetPath, filepath.Ext(targetPath)) + templ.TargetExt()
|
||||
contentReader = bytes.NewReader(content)
|
||||
}
|
||||
|
||||
|
@ -357,7 +367,7 @@ func getExcerpt(templ *markup.Template) string {
|
|||
}
|
||||
|
||||
// if we don't expect this to render to html don't bother parsing it
|
||||
if templ.Ext() != ".html" {
|
||||
if templ.TargetExt() != ".html" {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
|
@ -356,6 +356,166 @@ func TestRenderDataFile(t *testing.T) {
|
|||
</ul>`)
|
||||
}
|
||||
|
||||
func TestBuildTarget(t *testing.T) {
|
||||
config := newProject()
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
|
||||
// add base layout
|
||||
content := `---
|
||||
---
|
||||
<html>
|
||||
<head><title>{{page.title}}</title></head>
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
</html>`
|
||||
newFile(config.LayoutsDir, "base.html", content)
|
||||
|
||||
// add org post
|
||||
content = `---
|
||||
layout: base
|
||||
title: p1 - hello world!
|
||||
date: 2024-01-01
|
||||
---
|
||||
* Hello world!`
|
||||
newFile(config.SrcDir, "p1.org", content)
|
||||
|
||||
// add markdown post
|
||||
content = `---
|
||||
layout: base
|
||||
title: p2 - goodbye world!
|
||||
date: 2024-01-02
|
||||
---
|
||||
# Goodbye world!`
|
||||
newFile(config.SrcDir, "p2.md", content)
|
||||
|
||||
// add index page
|
||||
content = `---
|
||||
layout: base
|
||||
---
|
||||
<ul>{% for post in site.posts %}
|
||||
<li>{{post.title}}</li>{%endfor%}
|
||||
</ul>`
|
||||
newFile(config.SrcDir, "index.html", content)
|
||||
|
||||
// build site
|
||||
site, err := Load(*config)
|
||||
assertEqual(t, err, nil)
|
||||
err = site.Build()
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// test target files generated
|
||||
_, err = os.Stat(filepath.Join(config.TargetDir, "p1.html"))
|
||||
assertEqual(t, err, nil)
|
||||
_, err = os.Stat(filepath.Join(config.TargetDir, "p2.html"))
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// test index includes p1 and p2
|
||||
output, err := os.ReadFile(filepath.Join(config.TargetDir, "index.html"))
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<html><head><title></title></head>
|
||||
<body>
|
||||
<ul>
|
||||
<li>p2 - goodbye world!</li>
|
||||
<li>p1 - hello world!</li>
|
||||
</ul>
|
||||
|
||||
</body></html>`)
|
||||
}
|
||||
|
||||
func TestBuildWithDrafts(t *testing.T) {
|
||||
config := newProject()
|
||||
defer os.RemoveAll(config.RootDir)
|
||||
|
||||
// add base layout
|
||||
content := `---
|
||||
---
|
||||
<html>
|
||||
<head><title>{{page.title}}</title></head>
|
||||
<body>
|
||||
{{content}}
|
||||
</body>
|
||||
</html>`
|
||||
newFile(config.LayoutsDir, "base.html", content)
|
||||
|
||||
// add org post
|
||||
content = `---
|
||||
layout: base
|
||||
title: p1 - hello world!
|
||||
date: 2024-01-01
|
||||
---
|
||||
* Hello world!`
|
||||
newFile(config.SrcDir, "p1.org", content)
|
||||
|
||||
// add markdown post, make it draft
|
||||
content = `---
|
||||
layout: base
|
||||
title: p2 - goodbye world!
|
||||
date: 2024-01-02
|
||||
draft: true
|
||||
---
|
||||
# Goodbye world!`
|
||||
newFile(config.SrcDir, "p2.md", content)
|
||||
|
||||
// add index page
|
||||
content = `---
|
||||
layout: base
|
||||
---
|
||||
<ul>{% for post in site.posts %}
|
||||
<li>{{post.title}}</li>{%endfor%}
|
||||
</ul>`
|
||||
newFile(config.SrcDir, "index.html", content)
|
||||
|
||||
// build site with drafts
|
||||
config.IncludeDrafts = true
|
||||
site, err := Load(*config)
|
||||
assertEqual(t, err, nil)
|
||||
err = site.Build()
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// test target files generated
|
||||
_, err = os.Stat(filepath.Join(config.TargetDir, "p1.html"))
|
||||
assertEqual(t, err, nil)
|
||||
_, err = os.Stat(filepath.Join(config.TargetDir, "p2.html"))
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// test index includes p1 and p2
|
||||
output, err := os.ReadFile(filepath.Join(config.TargetDir, "index.html"))
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<html><head><title></title></head>
|
||||
<body>
|
||||
<ul>
|
||||
<li>p2 - goodbye world!</li>
|
||||
<li>p1 - hello world!</li>
|
||||
</ul>
|
||||
|
||||
</body></html>`)
|
||||
|
||||
// build site WITHOUT drafts
|
||||
config.IncludeDrafts = false
|
||||
site, err = Load(*config)
|
||||
assertEqual(t, err, nil)
|
||||
err = site.Build()
|
||||
assertEqual(t, err, nil)
|
||||
|
||||
// test only non drafts generated
|
||||
_, err = os.Stat(filepath.Join(config.TargetDir, "p1.html"))
|
||||
assertEqual(t, err, nil)
|
||||
_, err = os.Stat(filepath.Join(config.TargetDir, "p2.html"))
|
||||
assert(t, os.IsNotExist(err))
|
||||
|
||||
// test index includes p1 but NOT p2
|
||||
output, err = os.ReadFile(filepath.Join(config.TargetDir, "index.html"))
|
||||
assertEqual(t, err, nil)
|
||||
assertEqual(t, string(output), `<html><head><title></title></head>
|
||||
<body>
|
||||
<ul>
|
||||
<li>p1 - hello world!</li>
|
||||
</ul>
|
||||
|
||||
</body></html>`)
|
||||
}
|
||||
|
||||
// ------ HELPERS --------
|
||||
|
||||
func newProject() *config.Config {
|
||||
|
|
Loading…
Reference in a new issue