mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
Some minify configuration adjustments
This commit is contained in:
parent
574c2959b8
commit
7204b354a9
18 changed files with 173 additions and 143 deletions
|
@ -64,7 +64,7 @@ func (g *genDocsHelper) generate() error {
|
|||
enc := json.NewEncoder(f)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(docshelper.DocProviders); err != nil {
|
||||
if err := enc.Encode(docshelper.GetDocProvider()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -231,11 +231,6 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
|
|||
"duplicateTargetPaths",
|
||||
}
|
||||
|
||||
// Will set a value even if it is the default.
|
||||
flagKeysForced := []string{
|
||||
"minify",
|
||||
}
|
||||
|
||||
for _, key := range persFlagKeys {
|
||||
setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false)
|
||||
}
|
||||
|
@ -243,9 +238,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
|
|||
setValueFromFlag(cmd.Flags(), key, cfg, "", false)
|
||||
}
|
||||
|
||||
for _, key := range flagKeysForced {
|
||||
setValueFromFlag(cmd.Flags(), key, cfg, "", true)
|
||||
}
|
||||
setValueFromFlag(cmd.Flags(), "minify", cfg, "minifyOutput", true)
|
||||
|
||||
// Set some "config aliases"
|
||||
setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
|
||||
|
|
|
@ -193,6 +193,9 @@ markup
|
|||
menu
|
||||
: See [Add Non-content Entries to a Menu](/content-management/menus/#add-non-content-entries-to-a-menu).
|
||||
|
||||
minify
|
||||
: See [Configure Minify](#configure-minify)
|
||||
|
||||
module
|
||||
: Module config see [Module Config](/hugo-modules/configuration/).{{< new-in "0.56.0" >}}
|
||||
|
||||
|
@ -481,6 +484,14 @@ The above will try first to extract the value for `.Date` from the filename, the
|
|||
|
||||
Hugo v0.20 introduced the ability to render your content to multiple output formats (e.g., to JSON, AMP html, or CSV). See [Output Formats][] for information on how to add these values to your Hugo project's configuration file.
|
||||
|
||||
## Configure Minify
|
||||
|
||||
{{< new-in "0.68.0" >}}
|
||||
|
||||
Default configuration:
|
||||
|
||||
{{< code-toggle config="minify" />}}
|
||||
|
||||
## Configure File Caches
|
||||
|
||||
Since Hugo 0.52 you can configure more than just the `cacheDir`. This is the default configuration:
|
||||
|
|
|
@ -1415,7 +1415,7 @@
|
|||
"goldmark": {
|
||||
"renderer": {
|
||||
"hardWraps": false,
|
||||
"xHTML": false,
|
||||
"xhtml": false,
|
||||
"unsafe": false
|
||||
},
|
||||
"parser": {
|
||||
|
@ -1452,14 +1452,15 @@
|
|||
"footnoteReturnLinkContents": ""
|
||||
}
|
||||
},
|
||||
"minifiers": {
|
||||
"minify": {
|
||||
"minifyOutput": false,
|
||||
"disableHTML": false,
|
||||
"disableCSS": false,
|
||||
"disableJS": false,
|
||||
"disableJSON": false,
|
||||
"disableSVG": false,
|
||||
"disableXML": false,
|
||||
"tdewolff": {
|
||||
"enableHtml": true,
|
||||
"enableCss": true,
|
||||
"enableJs": true,
|
||||
"enableJson": true,
|
||||
"enableSvg": true,
|
||||
"enableXml": true,
|
||||
"html": {
|
||||
"keepConditionalComments": true,
|
||||
"keepDefaultAttrVals": true,
|
||||
|
|
|
@ -15,37 +15,37 @@
|
|||
// is of limited interest for the general Hugo user.
|
||||
package docshelper
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
type (
|
||||
DocProviderFunc = func() DocProvider
|
||||
DocProvider map[string]map[string]interface{}
|
||||
)
|
||||
|
||||
// DocProviders contains all DocProviders added to the system.
|
||||
var DocProviders = make(map[string]DocProvider)
|
||||
var docProviderFuncs []DocProviderFunc
|
||||
|
||||
// AddDocProvider adds or updates the DocProvider for a given name.
|
||||
func AddDocProvider(name string, provider DocProvider) {
|
||||
if prev, ok := DocProviders[name]; !ok {
|
||||
DocProviders[name] = provider
|
||||
} else {
|
||||
DocProviders[name] = merge(prev, provider)
|
||||
}
|
||||
func AddDocProviderFunc(fn DocProviderFunc) {
|
||||
docProviderFuncs = append(docProviderFuncs, fn)
|
||||
}
|
||||
|
||||
// DocProvider is used to save arbitrary JSON data
|
||||
// used for the generation of the documentation.
|
||||
type DocProvider func() map[string]interface{}
|
||||
func GetDocProvider() DocProvider {
|
||||
provider := make(DocProvider)
|
||||
|
||||
// MarshalJSON returns a JSON representation of the DocProvider.
|
||||
func (d DocProvider) MarshalJSON() ([]byte, error) {
|
||||
return json.MarshalIndent(d(), "", " ")
|
||||
for _, fn := range docProviderFuncs {
|
||||
p := fn()
|
||||
for k, v := range p {
|
||||
if prev, found := provider[k]; !found {
|
||||
provider[k] = v
|
||||
} else {
|
||||
merge(prev, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return provider
|
||||
}
|
||||
|
||||
func merge(a, b DocProvider) DocProvider {
|
||||
next := a()
|
||||
for k, v := range b() {
|
||||
next[k] = v
|
||||
}
|
||||
return func() map[string]interface{} {
|
||||
return next
|
||||
// Shallow merge
|
||||
func merge(dst, src map[string]interface{}) {
|
||||
for k, v := range src {
|
||||
dst[k] = v
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,8 +12,7 @@ import (
|
|||
// This is is just some helpers used to create some JSON used in the Hugo docs.
|
||||
func init() {
|
||||
|
||||
docsProvider := func() map[string]interface{} {
|
||||
docs := make(map[string]interface{})
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
|
||||
var chromaLexers []interface{}
|
||||
|
||||
|
@ -48,11 +47,11 @@ func init() {
|
|||
|
||||
chromaLexers = append(chromaLexers, lexerEntry)
|
||||
|
||||
docs["lexers"] = chromaLexers
|
||||
}
|
||||
return docs
|
||||
|
||||
return docshelper.DocProvider{"chroma": map[string]interface{}{"lexers": chromaLexers}}
|
||||
|
||||
}
|
||||
|
||||
docshelper.AddDocProvider("chroma", docsProvider)
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
|
|
|
@ -947,3 +947,33 @@ class-in-b {
|
|||
build("never", true)
|
||||
|
||||
}
|
||||
|
||||
func TestResourceMinifyDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := newTestSitesBuilder(t).WithConfigFile("toml", `
|
||||
baseURL = "https://example.org"
|
||||
|
||||
[minify]
|
||||
disableXML=true
|
||||
|
||||
|
||||
`)
|
||||
|
||||
b.WithContent("page.md", "")
|
||||
|
||||
b.WithSourceFile(
|
||||
"assets/xml/data.xml", "<root> <foo> asdfasdf </foo> </root>",
|
||||
)
|
||||
|
||||
b.WithTemplates("index.html", `
|
||||
{{ $xml := resources.Get "xml/data.xml" | minify | fingerprint }}
|
||||
XML: {{ $xml.Content | safeHTML }}|{{ $xml.RelPermalink }}
|
||||
`)
|
||||
|
||||
b.Build(BuildCfg{})
|
||||
|
||||
b.AssertFileContent("public/index.html", `
|
||||
XML: <root> <foo> asdfasdf </foo> </root>|/xml/data.min.3be4fddd19aaebb18c48dd6645215b822df74701957d6d36e59f203f9c30fd9f.xml
|
||||
`)
|
||||
}
|
||||
|
|
|
@ -94,11 +94,8 @@ var Default = Config{
|
|||
}
|
||||
|
||||
func init() {
|
||||
docsProvider := func() map[string]interface{} {
|
||||
docs := make(map[string]interface{})
|
||||
docs["markup"] = parser.LowerCaseCamelJSONMarshaller{Value: Default}
|
||||
return docs
|
||||
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
return docshelper.DocProvider{"config": map[string]interface{}{"markup": parser.LowerCaseCamelJSONMarshaller{Value: Default}}}
|
||||
}
|
||||
docshelper.AddDocProvider("config", docsProvider)
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
|
|
|
@ -6,12 +6,8 @@ import (
|
|||
|
||||
// This is is just some helpers used to create some JSON used in the Hugo docs.
|
||||
func init() {
|
||||
docsProvider := func() map[string]interface{} {
|
||||
docs := make(map[string]interface{})
|
||||
|
||||
docs["types"] = DefaultTypes
|
||||
return docs
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
return docshelper.DocProvider{"media": map[string]interface{}{"types": DefaultTypes}}
|
||||
}
|
||||
|
||||
docshelper.AddDocProvider("media", docsProvider)
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
package minifiers
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/docshelper"
|
||||
"github.com/gohugoio/hugo/parser"
|
||||
|
@ -61,36 +62,43 @@ type tdewolffConfig struct {
|
|||
XML xml.Minifier
|
||||
}
|
||||
|
||||
type minifiersConfig struct {
|
||||
EnableHTML bool
|
||||
EnableCSS bool
|
||||
EnableJS bool
|
||||
EnableJSON bool
|
||||
EnableSVG bool
|
||||
EnableXML bool
|
||||
type minifyConfig struct {
|
||||
// Whether to minify the published output (the HTML written to /public).
|
||||
MinifyOutput bool
|
||||
|
||||
DisableHTML bool
|
||||
DisableCSS bool
|
||||
DisableJS bool
|
||||
DisableJSON bool
|
||||
DisableSVG bool
|
||||
DisableXML bool
|
||||
|
||||
Tdewolff tdewolffConfig
|
||||
}
|
||||
|
||||
var defaultConfig = minifiersConfig{
|
||||
EnableHTML: true,
|
||||
EnableCSS: true,
|
||||
EnableJS: true,
|
||||
EnableJSON: true,
|
||||
EnableSVG: true,
|
||||
EnableXML: true,
|
||||
|
||||
var defaultConfig = minifyConfig{
|
||||
Tdewolff: defaultTdewolffConfig,
|
||||
}
|
||||
|
||||
func decodeConfig(cfg config.Provider) (conf minifiersConfig, err error) {
|
||||
func decodeConfig(cfg config.Provider) (conf minifyConfig, err error) {
|
||||
conf = defaultConfig
|
||||
|
||||
m := cfg.GetStringMap("minifiers")
|
||||
if m == nil {
|
||||
// May be set by CLI.
|
||||
conf.MinifyOutput = cfg.GetBool("minifyOutput")
|
||||
|
||||
v := cfg.Get("minify")
|
||||
if v == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy.
|
||||
if b, ok := v.(bool); ok {
|
||||
conf.MinifyOutput = b
|
||||
return
|
||||
}
|
||||
|
||||
m := maps.ToStringMap(v)
|
||||
|
||||
err = mapstructure.WeakDecode(m, &conf)
|
||||
|
||||
if err != nil {
|
||||
|
@ -101,11 +109,8 @@ func decodeConfig(cfg config.Provider) (conf minifiersConfig, err error) {
|
|||
}
|
||||
|
||||
func init() {
|
||||
docsProvider := func() map[string]interface{} {
|
||||
docs := make(map[string]interface{})
|
||||
docs["minifiers"] = parser.LowerCaseCamelJSONMarshaller{Value: defaultConfig}
|
||||
return docs
|
||||
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
return docshelper.DocProvider{"config": map[string]interface{}{"minify": parser.LowerCaseCamelJSONMarshaller{Value: defaultConfig}}}
|
||||
}
|
||||
docshelper.AddDocProvider("config", docsProvider)
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
package minifiers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
@ -26,8 +25,8 @@ func TestConfig(t *testing.T) {
|
|||
c := qt.New(t)
|
||||
v := viper.New()
|
||||
|
||||
v.Set("minifiers", map[string]interface{}{
|
||||
"enablexml": false,
|
||||
v.Set("minify", map[string]interface{}{
|
||||
"disablexml": true,
|
||||
"tdewolff": map[string]interface{}{
|
||||
"html": map[string]interface{}{
|
||||
"keepwhitespace": false,
|
||||
|
@ -36,10 +35,11 @@ func TestConfig(t *testing.T) {
|
|||
})
|
||||
|
||||
conf, err := decodeConfig(v)
|
||||
fmt.Println(conf)
|
||||
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
c.Assert(conf.MinifyOutput, qt.Equals, false)
|
||||
|
||||
// explicitly set value
|
||||
c.Assert(conf.Tdewolff.HTML.KeepWhitespace, qt.Equals, false)
|
||||
// default value
|
||||
|
@ -47,6 +47,19 @@ func TestConfig(t *testing.T) {
|
|||
c.Assert(conf.Tdewolff.CSS.KeepCSS2, qt.Equals, true)
|
||||
|
||||
// `enable` flags
|
||||
c.Assert(conf.EnableHTML, qt.Equals, true)
|
||||
c.Assert(conf.EnableXML, qt.Equals, false)
|
||||
c.Assert(conf.DisableHTML, qt.Equals, false)
|
||||
c.Assert(conf.DisableXML, qt.Equals, true)
|
||||
}
|
||||
|
||||
func TestConfigLegacy(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
v := viper.New()
|
||||
|
||||
// This was a bool < Hugo v0.58.
|
||||
v.Set("minify", true)
|
||||
|
||||
conf, err := decodeConfig(v)
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(conf.MinifyOutput, qt.Equals, true)
|
||||
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ import (
|
|||
|
||||
// Client wraps a minifier.
|
||||
type Client struct {
|
||||
// Whether output minification is enabled (HTML in /public)
|
||||
MinifyOutput bool
|
||||
|
||||
m *minify.M
|
||||
}
|
||||
|
||||
|
@ -62,30 +65,30 @@ func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provid
|
|||
|
||||
m := minify.New()
|
||||
if err != nil {
|
||||
return Client{m: m}, err
|
||||
return Client{}, err
|
||||
}
|
||||
|
||||
// We use the Type definition of the media types defined in the site if found.
|
||||
if conf.EnableCSS {
|
||||
if !conf.DisableCSS {
|
||||
addMinifier(m, mediaTypes, "css", &conf.Tdewolff.CSS)
|
||||
}
|
||||
if conf.EnableJS {
|
||||
if !conf.DisableJS {
|
||||
addMinifier(m, mediaTypes, "js", &conf.Tdewolff.JS)
|
||||
m.AddRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), &conf.Tdewolff.JS)
|
||||
}
|
||||
if conf.EnableJSON {
|
||||
if !conf.DisableJSON {
|
||||
addMinifier(m, mediaTypes, "json", &conf.Tdewolff.JSON)
|
||||
m.AddRegexp(regexp.MustCompile(`^(application|text)/(x-|ld\+)?json$`), &conf.Tdewolff.JSON)
|
||||
}
|
||||
if conf.EnableSVG {
|
||||
if !conf.DisableSVG {
|
||||
addMinifier(m, mediaTypes, "svg", &conf.Tdewolff.SVG)
|
||||
}
|
||||
if conf.EnableXML {
|
||||
if !conf.DisableXML {
|
||||
addMinifier(m, mediaTypes, "xml", &conf.Tdewolff.XML)
|
||||
}
|
||||
|
||||
// HTML
|
||||
if conf.EnableHTML {
|
||||
if !conf.DisableHTML {
|
||||
addMinifier(m, mediaTypes, "html", &conf.Tdewolff.HTML)
|
||||
for _, of := range outputFormats {
|
||||
if of.IsHTML {
|
||||
|
@ -94,7 +97,7 @@ func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provid
|
|||
}
|
||||
}
|
||||
|
||||
return Client{m: m}, nil
|
||||
return Client{m: m, MinifyOutput: conf.MinifyOutput}, nil
|
||||
}
|
||||
|
||||
func addMinifier(m *minify.M, mt media.Types, suffix string, min minify.Minifier) {
|
||||
|
|
|
@ -75,11 +75,11 @@ func TestNew(t *testing.T) {
|
|||
|
||||
}
|
||||
|
||||
func TestConfiguredMinify(t *testing.T) {
|
||||
func TestConfigureMinify(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
v := viper.New()
|
||||
v.Set("minifiers", map[string]interface{}{
|
||||
"enablexml": false,
|
||||
v.Set("minify", map[string]interface{}{
|
||||
"disablexml": true,
|
||||
"tdewolff": map[string]interface{}{
|
||||
"html": map[string]interface{}{
|
||||
"keepwhitespace": true,
|
||||
|
|
|
@ -10,15 +10,16 @@ import (
|
|||
|
||||
// This is is just some helpers used to create some JSON used in the Hugo docs.
|
||||
func init() {
|
||||
docsProvider := func() map[string]interface{} {
|
||||
docs := make(map[string]interface{})
|
||||
|
||||
docs["formats"] = DefaultFormats
|
||||
docs["layouts"] = createLayoutExamples()
|
||||
return docs
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
return docshelper.DocProvider{
|
||||
"output": map[string]interface{}{
|
||||
"formats": DefaultFormats,
|
||||
"layouts": createLayoutExamples(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
docshelper.AddDocProvider("output", docsProvider)
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
|
||||
func createLayoutExamples() interface{} {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"unicode"
|
||||
|
@ -35,6 +36,12 @@ func (c LowerCaseCamelJSONMarshaller) MarshalJSON() ([]byte, error) {
|
|||
converted := keyMatchRegex.ReplaceAllFunc(
|
||||
marshalled,
|
||||
func(match []byte) []byte {
|
||||
|
||||
// Attributes on the form XML, JSON etc.
|
||||
if bytes.Equal(match, bytes.ToUpper(match)) {
|
||||
return bytes.ToLower(match)
|
||||
}
|
||||
|
||||
// Empty keys are valid JSON, only lowercase if we do not have an
|
||||
// empty key.
|
||||
if len(match) > 2 {
|
||||
|
|
|
@ -68,21 +68,16 @@ type Descriptor struct {
|
|||
// DestinationPublisher is the default and currently only publisher in Hugo. This
|
||||
// publisher prepares and publishes an item to the defined destination, e.g. /public.
|
||||
type DestinationPublisher struct {
|
||||
fs afero.Fs
|
||||
minify bool
|
||||
min minifiers.Client
|
||||
fs afero.Fs
|
||||
min minifiers.Client
|
||||
}
|
||||
|
||||
// NewDestinationPublisher creates a new DestinationPublisher.
|
||||
func NewDestinationPublisher(fs afero.Fs, outputFormats output.Formats, mediaTypes media.Types, cfg config.Provider) (pub DestinationPublisher, err error) {
|
||||
pub = DestinationPublisher{fs: fs}
|
||||
minify := cfg.GetBool("minify")
|
||||
if minify {
|
||||
pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pub.minify = true
|
||||
pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -155,7 +150,7 @@ func (p DestinationPublisher) createTransformerChain(f Descriptor) transform.Cha
|
|||
|
||||
}
|
||||
|
||||
if p.minify {
|
||||
if p.min.MinifyOutput {
|
||||
minifyTransformer := p.min.Transformer(f.OutputFormat.MediaType)
|
||||
if minifyTransformer != nil {
|
||||
transformers = append(transformers, minifyTransformer)
|
||||
|
|
|
@ -41,23 +41,3 @@ func TestTransform(t *testing.T) {
|
|||
c.Assert(content, qt.Equals, "<h1>Hugo Rocks!</h1>")
|
||||
|
||||
}
|
||||
|
||||
func TestNoMinifier(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
spec, _ := htesting.NewTestResourceSpec()
|
||||
spec.Cfg.Set("minifiers.enableXML", false)
|
||||
client, _ := New(spec)
|
||||
|
||||
original := "<title> Hugo Rocks! </title>"
|
||||
r, err := htesting.NewResourceTransformerForSpec(spec, "hugo.xml", original)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
transformed, err := client.Minify(r)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
content, err := transformed.(resource.ContentProvider).Content()
|
||||
// error should be ignored because general users cannot control codes under `theme`s
|
||||
c.Assert(err, qt.IsNil)
|
||||
c.Assert(content, qt.Equals, original)
|
||||
}
|
||||
|
|
|
@ -24,8 +24,7 @@ import (
|
|||
|
||||
// This file provides documentation support and is randomly put into this package.
|
||||
func init() {
|
||||
docsProvider := func() map[string]interface{} {
|
||||
docs := make(map[string]interface{})
|
||||
docsProvider := func() docshelper.DocProvider {
|
||||
d := &deps.Deps{
|
||||
Cfg: viper.New(),
|
||||
Log: loggers.NewErrorLogger(),
|
||||
|
@ -41,11 +40,11 @@ func init() {
|
|||
|
||||
}
|
||||
|
||||
docs["funcs"] = namespaces
|
||||
return docs
|
||||
return docshelper.DocProvider{"tpl": map[string]interface{}{"funcs": namespaces}}
|
||||
|
||||
}
|
||||
|
||||
docshelper.AddDocProvider("tpl", docsProvider)
|
||||
docshelper.AddDocProviderFunc(docsProvider)
|
||||
}
|
||||
|
||||
func newTestConfig() *viper.Viper {
|
||||
|
|
Loading…
Reference in a new issue