Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

extras/shortcodes: Add Reuse Page Shortcode concept #3231

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docs/content/extras/shortcodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,44 @@ The `%` characters indicates that the shortcode's inner content needs further pr
The `<` character indicates that the shortcode's inner content doesn't need any further rendering, this will typically be pure HTML:

{{</* myshortcode */>}}<p>Hello <strong>World!</strong></p>{{</* /myshortcode */>}}

### Page Reuse shortcodes

The `~` character indiciates that the shortcode's inner content must be taken from other referred page. For example,
this will take the rendered content (including shortcodes/images/links etc.) of the page "mypage.md" located by path "/path/to/page"
starting from the content folder:

{{~/* myshortcode ref="/path/to/page/mypage" */~}}

Page Reuse shortcode must contain at least one parameter "ref" if _named parameters_ are used or the first one if _positional parameters_ are used
indicating the page to be reused. The page can be referred by an absolute path (starting from slash) or by a relative to the current directory path.
The page extension (.md) can be omitted as well as "/index.md" filename since it will be searched by Hugo engine if any.
If search is failed then Hugo will output an error and the shortcode will not be handled at all.

Page Reuse shortcodes don't require a template to be provided since they can act as built-in shortcodes just substituting themselves with the content
of the reused pages.
Note: If a template is omitted a closing shortcode will be omitted as well.

#### Page snippets
In order to have possibility to create pages that can be reused in different places but don't have its own served page on the site,
a new concept of page snippets has been introduced. To mark the page as a snippet you must add a parameter "issnippet=true" inside the front matter of the page.
This page will not be served by Hugo and instead can be used only in Page Reuse shortcodes.
When building your site you will see the output indicating how many page snippets your site will contain.

#### Links resolution
To provide flexibility in writing pages that can be reused in other pages there must be proper mechanism of links resolution.
This only concerns relative links since absolute links (starting from slash or even from a scheme like https://) are not considered
to be modified in any way.
So the relative links in the reused pages are resolved in 2 modes:
* Dynamic resolution - the links are resolved in the context of the page that specifies a Reuse Page shortcode.
* Relative resolution - the links are resolved in the context of the reused page.

The first one has bigger precedence.
Hugo will try to find the corresponding page or file that is included to the current built site.
If a link indicates a page and not a file (image/font etc.) the search will always expand to adding a .md, /index.md suffix
to find the most appropriate page. Links can be relative to the current directory or the current page file
(since the filename will be transformed to a part of the URL path).
If search is failed then Hugo will output a warning and won't transform the link.


## Built-in Shortcodes
Expand Down Expand Up @@ -251,6 +289,9 @@ parameters, named parameters work best. Allowing both types of parameters is
useful for complex layouts where you want to set default values that can be
overridden.

If you create a Reuse Page shortcode the first parameter will always be the "ref" parameter indicating the page to be reused.
It can be also referred by the first index since it always must go first if _positional parameters_ are used.

**Inside the template**

To access a parameter by position, the `.Get` method can be used:
Expand All @@ -276,6 +317,8 @@ of the content between the opening and closing shortcodes. If a closing
shortcode is required, you can check the length of `.Inner` and provide a warning
to the user.

If a Reuse Page shortcode is used, the variable `.InnerPage` will be populated with the content of the reused page for better control of the output.

A shortcode with `.Inner` content can be used without the inline content, and without the closing shortcode, by using the self-closing syntax:

{{</* innershortcode /*/>}}
Expand Down
4 changes: 4 additions & 0 deletions hugolib/hugo_sites.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,10 @@ func (h *HugoSites) setupTranslations() {
for i, site := range h.Sites {
// The site is assigned by language when read.
if site == p.s {
if p.Kind == KindSnippet {
site.PageSnippets = append(site.PageSnippets, p)
continue
}
site.updateBuildStats(p)
if shouldBuild {
site.Pages = append(site.Pages, p)
Expand Down
68 changes: 52 additions & 16 deletions hugolib/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ var (
)

const (
KindPage = "page"
KindPage = "page"
KindSnippet = "snippet"

// The rest are node types; home page, sections etc.
KindHome = "home"
Expand Down Expand Up @@ -248,6 +249,7 @@ type pageInit struct {
plainInit sync.Once
plainWordsInit sync.Once
renderingConfigInit sync.Once
markupTypeInit sync.Once
}

// IsNode returns whether this is an item of one of the list types in Hugo,
Expand Down Expand Up @@ -587,22 +589,50 @@ func (p *Page) setAutoSummary() error {
}

func (p *Page) renderContent(content []byte) []byte {
return p.renderContentWithResolvers(content, nil)
}

func (p *Page) renderReusedPage(content []byte, reusedPage *Page) []byte {
return p.renderContentWithResolvers(content, reusedPage)
}

func (p *Page) renderContentWithResolvers(content []byte, reusedPage *Page) []byte {
pageReuse := reusedPage != nil
resolveRelative := pageReuse || p.getRenderingConfig().SourceRelativeLinksEval
renderTOC := !pageReuse
var fn helpers.LinkResolverFunc
var fileFn helpers.FileResolverFunc
if p.getRenderingConfig().SourceRelativeLinksEval {
if resolveRelative {
fn = func(ref string) (string, error) {
return p.Site.SourceRelativeLink(ref, p)
// Trying to dynamically handle relative links (in case of page reuse).
// If not page reuse, just default handling
link, err := p.Site.SourceRelativeLink(ref, p, reusedPage)
if pageReuse && (err != nil || link == "") {
// If dynamic handling failed trying to handle relatively to reused page
return reusedPage.Site.SourceRelativeLink(ref, reusedPage, reusedPage)
}
return link, err
}
fileFn = func(ref string) (string, error) {
return p.Site.SourceRelativeLinkFile(ref, p)
// Trying to dynamically handle relative links (in case of page reuse).
// If not page reuse, just default handling
link, err := p.Site.SourceRelativeLinkFile(ref, p, reusedPage)
if pageReuse && (err != nil || link == "") {
// If dynamic handling failed trying to handle relatively to reused page
return reusedPage.Site.SourceRelativeLinkFile(ref, reusedPage, reusedPage)
}
return link, err
}
}

pageToRender := p
if pageReuse {
pageToRender = reusedPage
}
return p.s.ContentSpec.RenderBytes(&helpers.RenderingContext{
Content: content, RenderTOC: true, PageFmt: p.determineMarkupType(),
Cfg: p.Language(),
DocumentID: p.UniqueID(), DocumentName: p.Path(),
Config: p.getRenderingConfig(), LinkResolver: fn, FileResolver: fileFn})
Content: content, RenderTOC: renderTOC, PageFmt: pageToRender.determineMarkupType(),
Cfg: pageToRender.Language(),
DocumentID: pageToRender.UniqueID(), DocumentName: pageToRender.Path(),
Config: pageToRender.getRenderingConfig(), LinkResolver: fn, FileResolver: fileFn})
}

func (p *Page) getRenderingConfig() *helpers.Blackfriday {
Expand Down Expand Up @@ -962,6 +992,11 @@ func (p *Page) update(f interface{}) error {
case "iscjklanguage":
isCJKLanguage = new(bool)
*isCJKLanguage = cast.ToBool(v)
case "issnippet":
if cast.ToBool(v) {
p.Kind = KindSnippet
}

default:
// If not one of the explicit values, store in Params
switch vv := v.(type) {
Expand Down Expand Up @@ -1253,13 +1288,14 @@ func (p *Page) Menus() PageMenus {
}

func (p *Page) determineMarkupType() string {
// Try markup explicitly set in the frontmatter
p.Markup = helpers.GuessType(p.Markup)
if p.Markup == "unknown" {
// Fall back to file extension (might also return "unknown")
p.Markup = helpers.GuessType(p.Source.Ext())
}

p.markupTypeInit.Do(func() {
// Try markup explicitly set in the frontmatter
p.Markup = helpers.GuessType(p.Markup)
if p.Markup == "unknown" {
// Fall back to file extension (might also return "unknown")
p.Markup = helpers.GuessType(p.Source.Ext())
}
})
return p.Markup
}

Expand Down
3 changes: 3 additions & 0 deletions hugolib/page_collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type PageCollections struct {

// Includes absolute all pages (of all types), including drafts etc.
rawAllPages Pages

// Page snippets that can be used only for Page reuse. Ignored by default.
PageSnippets Pages
}

func (c *PageCollections) refreshPageCaches() {
Expand Down
Loading