mirror of https://github.com/jlelse/GoBlog
Remove useless template includes and start rendering parts of the HTML directly with Go instead of templates
This commit is contained in:
parent
a3517a9a97
commit
fa82364b70
2
go.mod
2
go.mod
|
@ -55,7 +55,7 @@ require (
|
|||
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
|
||||
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f
|
||||
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/text v0.3.7
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
|
|
4
go.sum
4
go.sum
|
@ -533,8 +533,8 @@ golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
|
||||
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d h1:62NvYBuaanGXR2ZOfwDFkhhl6X1DUgf8qg3GuQvxZsE=
|
||||
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
|
|
@ -63,6 +63,12 @@ func (a *goBlog) initRendering() error {
|
|||
"liketitle": a.likeTitle,
|
||||
"photolinks": a.photoLinks,
|
||||
"gettrack": a.getTrack,
|
||||
// Code based rendering
|
||||
"posttax": a.renderPostTax,
|
||||
"oldcontentwarning": a.renderOldContentWarning,
|
||||
"interactions": a.renderInteractions,
|
||||
"author": a.renderAuthor,
|
||||
"tor": a.renderTorNotice,
|
||||
// Others
|
||||
"dateformat": dateFormat,
|
||||
"isodate": isoDateFormat,
|
||||
|
@ -72,9 +78,7 @@ func (a *goBlog) initRendering() error {
|
|||
"string": a.ts.GetTemplateStringVariantFunc(),
|
||||
"include": a.includeRenderedTemplate,
|
||||
"urlize": urlize,
|
||||
"sort": sortedStrings,
|
||||
"absolute": a.getFullAddress,
|
||||
"mentions": a.db.getWebmentionsByAddress,
|
||||
"geotitle": a.geoTitle,
|
||||
"geolink": geoOSMLink,
|
||||
"opensearch": openSearchUrl,
|
||||
|
|
|
@ -21,10 +21,10 @@ type assetFile struct {
|
|||
body []byte
|
||||
}
|
||||
|
||||
func (a *goBlog) initTemplateAssets() (err error) {
|
||||
func (a *goBlog) initTemplateAssets() error {
|
||||
a.assetFileNames = map[string]string{}
|
||||
a.assetFiles = map[string]*assetFile{}
|
||||
err = filepath.Walk(assetsFolder, func(path string, info os.FileInfo, err error) error {
|
||||
if err := filepath.Walk(assetsFolder, func(path string, info os.FileInfo, err error) error {
|
||||
if info.Mode().IsRegular() {
|
||||
// Open file
|
||||
file, err := os.Open(path)
|
||||
|
@ -43,13 +43,11 @@ func (a *goBlog) initTemplateAssets() (err error) {
|
|||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
// Add syntax highlighting CSS
|
||||
err = a.initChromaCSS()
|
||||
if err != nil {
|
||||
if err := a.initChromaCSS(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{{ define "author" }}
|
||||
{{ with .User }}
|
||||
<div class="p-author h-card hide">
|
||||
{{ with .Picture }}<data class="u-photo" value="{{ . }}"></data>{{ end }}
|
||||
{{ if .Name }}
|
||||
<a href="{{ with .Link }}{{ . }}{{ else }}/{{ end }}" class="p-name u-url" rel="me">{{ .Name }}</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -5,9 +5,7 @@
|
|||
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||
<link rel="stylesheet" href="{{ asset "css/styles.css" }}">
|
||||
{{ with .Canonical }}<link rel="canonical" href="{{ . }}">{{ end }}
|
||||
{{ block "title" . }}
|
||||
<title>{{ mdtitle .Blog.Title }}</title>
|
||||
{{ end }}
|
||||
{{ block "title" . }}<title>{{ mdtitle .Blog.Title }}</title>{{ end }}
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS ({{ mdtitle .Blog.Title }})" href="{{ .Blog.Path }}.rss"/>
|
||||
<link rel="alternate" type="application/atom+xml" title="Atom ({{ mdtitle .Blog.Title }})" href="{{ .Blog.Path }}.atom"/>
|
||||
<link rel="alternate" type="application/feed+json" title="JSON Feed ({{ mdtitle .Blog.Title }})" href="{{ .Blog.Path }}.json"/>
|
||||
|
@ -17,14 +15,32 @@
|
|||
<link rel="token_endpoint" href="/indieauth/token" />
|
||||
{{ with .User }}{{ range .Identities }}<link rel="me" href="{{ . }}" />{{ end }}{{ end }}
|
||||
{{ $os := opensearch .Blog }}
|
||||
{{ if $os }}
|
||||
<link rel="search" type="application/opensearchdescription+xml" href="{{ $os }}" title="{{ mdtitle .Blog.Title }}" />
|
||||
{{ end }}
|
||||
{{ include "header" . }}
|
||||
{{ if $os }}<link rel="search" type="application/opensearchdescription+xml" href="{{ $os }}" title="{{ mdtitle .Blog.Title }}" />{{ end }}
|
||||
{{ with .Blog.Announcement }}{{ with .Text }}<div id="announcement" data-nosnippet>{{ md . }}</div>{{ end }}{{ end }}
|
||||
<header>
|
||||
<h1><a href="{{ .Blog.RelativePath "/" }}" rel="home" title="{{ mdtitle .Blog.Title }}" translate="no">{{ mdtitle .Blog.Title }}</a></h1>
|
||||
{{ with .Blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
|
||||
{{ with index .Blog.Menus "main" }}
|
||||
<nav>{{ range $i, $item := .Items }}{{ if ne $i 0 }} • {{ end }}<a href="{{ $item.Link }}">{{ mdtitle $item.Title }}</a>{{ end }}</nav>
|
||||
{{ end }}
|
||||
{{ if .LoggedIn }}
|
||||
<nav>
|
||||
<a href="{{ .Blog.RelativePath "/editor" }}">{{ string .Blog.Lang "editor" }}</a>
|
||||
• <a href="/notifications">{{ string .Blog.Lang "notifications" }}</a>
|
||||
{{ if .WebmentionReceivingEnabled }}• <a href="/webmention">{{ string .Blog.Lang "webmentions" }}</a>{{ end }}
|
||||
{{ if .CommentsEnabled }}• <a href="{{ .Blog.RelativePath "/comment" }}">{{ string .Blog.Lang "comments" }}</a>{{ end }}
|
||||
• <a href="/logout">{{ string .Blog.Lang "logout" }}</a>
|
||||
</nav>
|
||||
{{ end }}
|
||||
</header>
|
||||
{{ block "main" . }}{{ end }}
|
||||
{{ include "footer" . }}
|
||||
{{ if .EasterEgg }}
|
||||
<script defer src="{{ asset "js/easteregg.js" }}"></script>
|
||||
{{ end }}
|
||||
<footer>
|
||||
{{ with index .Blog.Menus "footer" }}
|
||||
<nav>{{ range $i, $item := .Items }}{{ if ne $i 0 }} • {{ end }}<a href="{{ $item.Link }}">{{ mdtitle $item.Title }}</a>{{ end }}</nav>
|
||||
{{ end }}
|
||||
<p translate="no">© {{ dateformat now "2006" }} {{ with .User.Name }}{{ . }}{{ else }}{{ mdtitle .Blog.Title }}{{ end }}</p>
|
||||
{{ tor .Blog .TorUsed .TorAddress }}
|
||||
</footer>
|
||||
{{ if .EasterEgg }}<script defer src="{{ asset "js/easteregg.js" }}"></script>{{ end }}
|
||||
</html>
|
||||
{{ end }}
|
|
@ -21,9 +21,7 @@
|
|||
</ul>
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ if .CommentsEnabled }}
|
||||
{{ include "interactions" . }}
|
||||
{{ end }}
|
||||
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "blogroll" }}
|
||||
|
|
|
@ -9,9 +9,7 @@
|
|||
<p id="loading" data-table="{{.Data.TableUrl}}">{{ string .Blog.Lang "loading" }}</p>
|
||||
<script defer src="{{ asset "js/blogstats.js" }}"></script>
|
||||
</main>
|
||||
{{ if .CommentsEnabled }}
|
||||
{{ include "interactions" . }}
|
||||
{{ end }}
|
||||
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "blogstats" }}
|
||||
|
|
|
@ -13,9 +13,7 @@
|
|||
{{ html .Data.Comment }}
|
||||
</p>
|
||||
</main>
|
||||
{{ if .CommentsEnabled }}
|
||||
{{ include "interactions" . }}
|
||||
{{ end }}
|
||||
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "comment" }}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
{{ define "editorpreview" }}
|
||||
{{ with .Data.RenderedTitle }}<h1>{{ . }}</h1>{{ end }}
|
||||
{{ include "summaryandpostmeta" . }}
|
||||
{{ if .Data.Content }}
|
||||
<div>
|
||||
{{ content .Data true }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ include "posttax" . }}
|
||||
{{ if .Data.Content }}<div>{{ content .Data true }}</div>{{ end }}
|
||||
{{ posttax .Data .Blog }}
|
||||
{{ end }}
|
|
@ -1,16 +0,0 @@
|
|||
{{ define "footer" }}
|
||||
<footer>
|
||||
{{ with index .Blog.Menus "footer" }}
|
||||
<nav>{{ range $i, $item := .Items }}{{ if ne $i 0 }} • {{ end }}<a href="{{ $item.Link }}">{{ mdtitle $item.Title }}</a>{{ end }}
|
||||
</nav>
|
||||
{{ end }}
|
||||
<p translate="no">© {{ dateformat now "2006" }} {{ with .User.Name }}{{ . }}{{ else }}{{ mdtitle .Blog.Title }}{{ end }}</p>
|
||||
{{ if .TorUsed }}
|
||||
<p id="tor">🔐 {{ string .Blog.Lang "connectedviator" }}</p>
|
||||
{{ else }}
|
||||
{{ if .TorAddress }}
|
||||
<p id="tor">🔓 <a href="{{ .TorAddress }}">{{ string .Blog.Lang "connectviator" }}</a> <a href="https://www.torproject.org/" target="_blank" rel="nofollow noopener noreferrer">{{ string .Blog.Lang "whatistor" }}</a></p>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</footer>
|
||||
{{ end }}
|
|
@ -21,9 +21,7 @@
|
|||
<script defer src="{{ asset "js/geomap.js" }}"></script>
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ if .CommentsEnabled }}
|
||||
{{ include "interactions" . }}
|
||||
{{ end }}
|
||||
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "geomap" }}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
{{ define "header" }}
|
||||
{{ with .Blog.Announcement }}{{ with .Text }}
|
||||
<div id="announcement" data-nosnippet>
|
||||
{{ md . }}
|
||||
</div>
|
||||
{{ end }}{{ end }}
|
||||
<header>
|
||||
<h1><a href="{{ .Blog.RelativePath "/" }}" rel="home" title="{{ mdtitle .Blog.Title }}" translate="no">{{ mdtitle .Blog.Title }}</a></h1>
|
||||
{{ with .Blog.Description }}<p><i>{{ . }}</i></p>{{ end }}
|
||||
<nav>
|
||||
{{ with index .Blog.Menus "main" }}
|
||||
{{ range $i, $item := .Items }}{{ if ne $i 0 }} • {{ end }}<a href="{{ $item.Link }}">{{ mdtitle $item.Title }}</a>{{ end }}
|
||||
{{ end }}
|
||||
</nav>
|
||||
{{ if .LoggedIn }}
|
||||
<nav>
|
||||
<a href="{{ .Blog.RelativePath "/editor" }}">{{ string .Blog.Lang "editor" }}</a>
|
||||
• <a href="/notifications">{{ string .Blog.Lang "notifications" }}</a>
|
||||
{{ if .WebmentionReceivingEnabled }}• <a href="/webmention">{{ string .Blog.Lang "webmentions" }}</a>{{ end }}
|
||||
{{ if .CommentsEnabled }}• <a href="{{ .Blog.RelativePath "/comment" }}">{{ string .Blog.Lang "comments" }}</a>{{ end }}
|
||||
• <a href="/logout">{{ string .Blog.Lang "logout" }}</a>
|
||||
</nav>
|
||||
{{ end }}
|
||||
</header>
|
||||
{{ end }}
|
|
@ -27,7 +27,7 @@
|
|||
{{ if .Data.HasNext }}
|
||||
<p><a href="{{ .Data.Next }}">{{ string .Blog.Lang "next" }}</a></p>
|
||||
{{ end }}
|
||||
{{ include "author" . }}
|
||||
{{ author }}
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
{{ define "interactions" }}
|
||||
<details class="p" id="interactions">
|
||||
<summary><b>{{ string .Blog.Lang "interactions" }}</b></summary>
|
||||
{{ $rd := . }}
|
||||
{{ with ( mentions .Canonical ) }}
|
||||
{{ include "mentions" $rd . }}
|
||||
{{ end }}
|
||||
<form class="fw p" method="post" action="/webmention">
|
||||
<label for="wm-source" class="p">{{ string .Blog.Lang "interactionslabel" }}</label>
|
||||
<input id="wm-source" type="url" name="source" placeholder="URL" required>
|
||||
<input type="hidden" name="target" value="{{ .Canonical }}">
|
||||
<input type="submit" value="{{ string .Blog.Lang "send" }}">
|
||||
</form>
|
||||
<form class="fw p" method="post" action="{{ .Blog.RelativePath "/comment" }}">
|
||||
<input type="hidden" name="target" value="{{ .Canonical }}">
|
||||
<input type="text" name="name" placeholder="{{ string .Blog.Lang "nameopt" }}">
|
||||
<input type="url" name="website" placeholder="{{ string .Blog.Lang "websiteopt" }}">
|
||||
<textarea name="comment" required placeholder="{{ string .Blog.Lang "comment" }}"></textarea>
|
||||
<input type="submit" value="{{ string .Blog.Lang "docomment" }}">
|
||||
</form>
|
||||
</details>
|
||||
{{ end }}
|
|
@ -17,7 +17,7 @@
|
|||
{{ end }}
|
||||
<input type="submit" value="{{ string .Blog.Lang "login" }}">
|
||||
</form>
|
||||
{{ include "author" . }}
|
||||
{{ author }}
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
{{ define "mentions" }}
|
||||
{{ $rd := . }}
|
||||
<ul>
|
||||
{{ range $i, $mention := .Data }}
|
||||
<li>
|
||||
<a href="{{ $mention.Url }}" target="_blank" rel="nofollow noopener noreferrer ugc">
|
||||
{{ if $mention.Author }}
|
||||
{{ $mention.Author }}
|
||||
{{ else }}
|
||||
{{ $mention.Url }}
|
||||
{{ end }}
|
||||
</a>
|
||||
{{ with $mention.Title }} <b>{{.}}</b>{{ end }}
|
||||
{{ with $mention.Content }} <i>{{.}}</i>{{ end }}
|
||||
{{ with $mention.Submentions }}{{ include "mentions" $rd . }}{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
|
@ -1,5 +0,0 @@
|
|||
{{ define "oldcontentwarning" }}
|
||||
{{ if .Data.Old }}
|
||||
<strong class="p border-top border-bottom">{{ string .Blog.Lang "oldcontent" }}</strong>
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -3,7 +3,10 @@
|
|||
<title>{{ with .Data.RenderedTitle }}{{ . }} - {{end}}{{ mdtitle .Blog.Title }}</title>
|
||||
{{ include "postheadmeta" . }}
|
||||
{{ with shorturl .Data }}<link rel="shortlink" href="{{ . }}">{{ end }}
|
||||
{{ include "trackheader" . }}
|
||||
{{ if .Data.HasTrack }}
|
||||
<link rel="stylesheet" href="/-/leaflet/leaflet.css"/>
|
||||
<script src="/-/leaflet/leaflet.js"></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
|
@ -11,18 +14,40 @@
|
|||
<article>
|
||||
<data class="u-url hide" value="{{ absolute .Data.Path }}"></data>
|
||||
{{ with .Data.RenderedTitle }}<h1 class=p-name>{{ . }}</h1>{{ end }}
|
||||
{{ include "postmeta" . }}
|
||||
{{ include "postactions" . }}
|
||||
<div class="p">
|
||||
{{ include "summaryandpostmeta" . }}
|
||||
{{ $translations := (translations .Data) }}
|
||||
{{ if gt (len $translations) 0 }}
|
||||
<div>{{ string .Blog.Lang "translations" }}: {{ $delimiter := "" }}{{ range $i, $t := $translations }}{{ $delimiter }}<a href="{{ $t.Path }}" translate="no">{{ $t.RenderedTitle }}</a>{{ $delimiter = ", " }}{{ end }}</div>
|
||||
{{ end }}
|
||||
{{ $short := shorturl .Data }}
|
||||
{{ if $short }}<div>{{ string .Blog.Lang "shorturl" }} <a href="{{ $short }}" rel="shortlink">{{ $short }}</a></div>{{ end }}
|
||||
{{ if ne .Data.Status "published" }}<div>{{ string .Blog.Lang "status" }}: {{ .Data.Status }}</div>{{ end }}
|
||||
</div>
|
||||
<div id="post-actions">
|
||||
<a href="https://www.addtoany.com/share#url={{ shorturl .Data }}{{ with .Data.RenderedTitle }}&title={{ . }}{{ end }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "share" }}</a>
|
||||
<a id="translateBtn" href="https://translate.google.com/translate?u={{ absolute .Data.Path }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "translate" }}</a>
|
||||
<script defer src="{{ asset "js/translate.js" }}"></script>
|
||||
<button id="speakBtn" class="hide" data-speak="{{ string .Blog.Lang "speak" }}" data-stopspeak="{{ string .Blog.Lang "stopspeak" }}"></button>
|
||||
<script defer src="{{ if .Data.TTS }}{{ asset "js/tts.js" }}{{ else }}{{ asset "js/speak.js" }}{{ end }}"></script>
|
||||
</div>
|
||||
{{ if .Data.TTS }}<div class="p hide" id="tts"><audio controls preload=none id="tts-audio"><source src="{{ .Data.TTS }}"/></audio></div>{{ end }}
|
||||
{{ if .Data.Content }}
|
||||
{{ include "oldcontentwarning" . }}
|
||||
<div class=e-content>
|
||||
{{ content .Data false }}
|
||||
</div>
|
||||
{{ oldcontentwarning .Data .Blog }}
|
||||
<div class=e-content>{{ content .Data false }}</div>
|
||||
{{ end }}
|
||||
{{ include "trackdetails" . }}
|
||||
{{ include "posttax" . }}
|
||||
{{ if .Data.HasTrack }}
|
||||
{{ $track := (gettrack .Data) }}
|
||||
{{ if $track }}{{ if $track.HasPoints }}
|
||||
{{ $lang := .Blog.Lang }}
|
||||
<p>{{ with $track.Name }}<b>{{ . }}</b> {{ end }}{{ with $track.Kilometers }}🏁 {{ . }} {{ string $lang "kilometers" }} {{ end }}{{ with $track.Hours }}⌛ {{ . }}{{ end }}</p>
|
||||
<div class="p" id="map" data-paths="{{ $track.PathsJSON }}" data-points="{{ $track.PointsJSON }}" data-minzoom={{ $track.MinZoom }} data-maxzoom={{ $track.MaxZoom }} data-attribution="{{ $track.MapAttribution }}"></div>
|
||||
<script defer src="{{ asset "js/geotrack.js" }}"></script>
|
||||
{{ end }}{{ end }}
|
||||
{{ end }}
|
||||
{{ posttax .Data .Blog }}
|
||||
</article>
|
||||
{{ include "author" . }}
|
||||
{{ author }}
|
||||
</main>
|
||||
{{ if .LoggedIn }}
|
||||
<div id="posteditactions" class="p">
|
||||
|
@ -53,9 +78,7 @@
|
|||
<script defer src="{{ asset "js/formconfirm.js" }}"></script>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .CommentsEnabled }}
|
||||
{{ include "interactions" . }}
|
||||
{{ end }}
|
||||
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "post" }}
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
{{ define "postactions" }}
|
||||
<div id="post-actions">
|
||||
<a href="https://www.addtoany.com/share#url={{ shorturl .Data }}{{ with .Data.RenderedTitle }}&title={{ . }}{{ end }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "share" }}</a>
|
||||
<a id="translateBtn" href="https://translate.google.com/translate?u={{ absolute .Data.Path }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "translate" }}</a>
|
||||
<script defer src="{{ asset "js/translate.js" }}"></script>
|
||||
<button id="speakBtn" class="hide" data-speak="{{ string .Blog.Lang "speak" }}" data-stopspeak="{{ string .Blog.Lang "stopspeak" }}"></button>
|
||||
{{ if .Data.TTS }}
|
||||
<script defer src="{{ asset "js/tts.js" }}"></script>
|
||||
{{ else }}
|
||||
<script defer src="{{ asset "js/speak.js" }}"></script>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if .Data.TTS }}
|
||||
<div class="p hide" id="tts">
|
||||
<audio controls preload=none id="tts-audio"><source src="{{ .Data.TTS }}"/></audio>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -1,12 +0,0 @@
|
|||
{{ define "postmeta" }}
|
||||
<div class="p">
|
||||
{{ include "summaryandpostmeta" . }}
|
||||
{{ $translations := (translations .Data) }}
|
||||
{{ if gt (len $translations) 0 }}
|
||||
<div>{{ string .Blog.Lang "translations" }}: {{ $delimiter := "" }}{{ range $i, $t := $translations }}{{ $delimiter }}<a href="{{ $t.Path }}" translate="no">{{ $t.RenderedTitle }}</a>{{ $delimiter = ", " }}{{ end }}</div>
|
||||
{{ end }}
|
||||
{{ $short := shorturl .Data }}
|
||||
{{ if $short }}<div>{{ string .Blog.Lang "shorturl" }} <a href="{{ $short }}" rel="shortlink">{{ $short }}</a></div>{{ end }}
|
||||
{{ if ne .Data.Status "published" }}<div>{{ string .Blog.Lang "status" }}: {{ .Data.Status }}</div>{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
|
@ -1,15 +0,0 @@
|
|||
{{ define "posttax" }}
|
||||
{{ $post := .Data }}
|
||||
{{ $blog := .Blog }}
|
||||
{{ range $i, $tax := $blog.Taxonomies }}
|
||||
{{ $tvs := sort (ps $post $tax.Name) }}
|
||||
{{ if $tvs }}{{ if not (eq (len $tvs) 0) }}
|
||||
<p>
|
||||
<b>{{ mdtitle $tax.Title }}</b>:
|
||||
{{ range $j, $tv := $tvs }}
|
||||
<a class="p-category" rel="tag" href="{{ $blog.RelativePath ( printf "/%s/%s" $tax.Name (urlize $tv) ) }}">{{ mdtitle $tv }}</a>
|
||||
{{ end }}
|
||||
</p>
|
||||
{{ end }}{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -7,13 +7,9 @@
|
|||
<main class=h-entry>
|
||||
<article>
|
||||
<data class="u-url hide" value="{{ absolute .Data.Path }}"></data>
|
||||
{{ if .Data.Content }}
|
||||
<div class=e-content>
|
||||
{{ content .Data false }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .Data.Content }}<div class=e-content>{{ content .Data false }}</div>{{ end }}
|
||||
</article>
|
||||
{{ include "author" . }}
|
||||
{{ author }}
|
||||
</main>
|
||||
{{ if .LoggedIn }}
|
||||
<div id="posteditactions" class="p">
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
{{ define "trackdetails" }}
|
||||
{{ if .Data.HasTrack }}
|
||||
{{ $track := (gettrack .Data) }}
|
||||
{{ if $track }}
|
||||
{{ if $track.HasPoints }}
|
||||
{{ $lang := .Blog.Lang }}
|
||||
<p>{{ with $track.Name }}<b>{{ . }}</b> {{ end }}{{ with $track.Kilometers }}🏁 {{ . }} {{ string $lang "kilometers" }} {{ end }}{{ with $track.Hours }}⌛ {{ . }}{{ end }}</p>
|
||||
<div class="p" id="map"
|
||||
data-paths="{{ $track.PathsJSON }}"
|
||||
data-points="{{ $track.PointsJSON }}"
|
||||
data-minzoom={{ $track.MinZoom }}
|
||||
data-maxzoom={{ $track.MaxZoom }}
|
||||
data-attribution="{{ $track.MapAttribution }}"
|
||||
></div>
|
||||
<script defer src="{{ asset "js/geotrack.js" }}"></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -1,6 +0,0 @@
|
|||
{{ define "trackheader" }}
|
||||
{{ if .Data.HasTrack }}
|
||||
<link rel="stylesheet" href="/-/leaflet/leaflet.css"/>
|
||||
<script src="/-/leaflet/leaflet.js"></script>
|
||||
{{ end }}
|
||||
{{ end }}
|
|
@ -0,0 +1 @@
|
|||
<details class="p" id="interactions"><summary><strong>Interactions & Comments</strong></summary><ul><li><a href="https://example.com/testpost2" target="_blank" rel="nofollow noopener noreferrer ugc">https://example.com/testpost2</a> <strong>Test-Title</strong> <i>Test</i><ul><li><a href="https://example.com/testpost3" target="_blank" rel="nofollow noopener noreferrer ugc">https://example.com/testpost3</a> <strong>Test-Title</strong> <i>Test</i></li></ul></li></ul><form class="fw p" method="post" action="/webmention"><label for="wm-source" class="p">Have you published a response to this? Paste the URL here.</label><input id="wm-source" type="url" name="source" placeholder="URL" required=""><input type="hidden" name="target" value="https://example.com/testpost1"><input type="submit" value="Send (to review)"></form><form class="fw p" method="post" action="/comment"><input type="hidden" name="target" value="https://example.com/testpost1"><input type="text" name="name" placeholder="Name (optional)"><input type="url" name="website" placeholder="Website (optional)"><textarea name="comment" required="" placeholder="Comment"></textarea><input type="submit" value="Comment"></form></details>
|
4
tts.go
4
tts.go
|
@ -106,7 +106,9 @@ func (a *goBlog) createPostTTSAudio(p *post) error {
|
|||
|
||||
// Merge partsBuffers into final buffer
|
||||
var final bytes.Buffer
|
||||
mp3merge.MergeMP3(&final, partsBuffers...)
|
||||
if err := mp3merge.MergeMP3(&final, partsBuffers...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save audio
|
||||
audioReader := bytes.NewReader(final.Bytes())
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// This file includes some functions that render parts of the HTML
|
||||
|
||||
type htmlBuilder struct {
|
||||
strings.Builder
|
||||
}
|
||||
|
||||
func (h *htmlBuilder) write(s string) {
|
||||
_, _ = h.WriteString(s)
|
||||
}
|
||||
|
||||
func (h *htmlBuilder) writeEscaped(s string) {
|
||||
if len(s) == 0 {
|
||||
return
|
||||
}
|
||||
template.HTMLEscape(h, []byte(s))
|
||||
}
|
||||
|
||||
func (h *htmlBuilder) writeAttribute(attr, val string) {
|
||||
h.write(` `)
|
||||
h.write(attr)
|
||||
h.write(`="`)
|
||||
h.writeEscaped(val)
|
||||
h.write(`"`)
|
||||
}
|
||||
|
||||
func (h *htmlBuilder) writeElementOpen(tag string, attrs ...string) {
|
||||
h.write(`<`)
|
||||
h.write(tag)
|
||||
for i := 0; i < len(attrs); i += 2 {
|
||||
h.writeAttribute(attrs[i], attrs[i+1])
|
||||
}
|
||||
h.write(`>`)
|
||||
}
|
||||
|
||||
func (h *htmlBuilder) writeElementClose(tag string) {
|
||||
h.write(`</`)
|
||||
h.write(tag)
|
||||
h.write(`>`)
|
||||
}
|
||||
|
||||
func (h *htmlBuilder) html() template.HTML {
|
||||
return template.HTML(h.String())
|
||||
}
|
||||
|
||||
// Render the HTML to show the list of post taxonomy values (tags, series, etc.)
|
||||
func (a *goBlog) renderPostTax(p *post, b *configBlog) template.HTML {
|
||||
if b == nil || p == nil {
|
||||
return ""
|
||||
}
|
||||
var hb htmlBuilder
|
||||
// Iterate over all taxonomies
|
||||
for _, tax := range b.Taxonomies {
|
||||
// Get all sorted taxonomy values for this post
|
||||
if taxValues := sortedStrings(p.Parameters[tax.Name]); len(taxValues) > 0 {
|
||||
// Start new paragraph
|
||||
hb.writeElementOpen("p")
|
||||
// Add taxonomy name
|
||||
hb.writeElementOpen("strong")
|
||||
hb.writeEscaped(a.renderMdTitle(tax.Title))
|
||||
hb.writeElementClose("strong")
|
||||
hb.write(": ")
|
||||
// Add taxonomy values
|
||||
for i, taxValue := range taxValues {
|
||||
if i > 0 {
|
||||
hb.write(", ")
|
||||
}
|
||||
hb.writeElementOpen(
|
||||
"a",
|
||||
"class", "p-category",
|
||||
"rel", "tag",
|
||||
"href", b.getRelativePath(fmt.Sprintf("/%s/%s", tax.Name, urlize(taxValue))),
|
||||
)
|
||||
hb.writeEscaped(a.renderMdTitle(taxValue))
|
||||
hb.writeElementClose("a")
|
||||
}
|
||||
// End paragraph
|
||||
hb.writeElementClose("p")
|
||||
}
|
||||
}
|
||||
return hb.html()
|
||||
}
|
||||
|
||||
// Render the HTML to show a warning for old posts
|
||||
func (a *goBlog) renderOldContentWarning(p *post, b *configBlog) template.HTML {
|
||||
if b == nil || p == nil || !p.Old() {
|
||||
return ""
|
||||
}
|
||||
var hb htmlBuilder
|
||||
hb.writeElementOpen("strong", "class", "p border-top border-bottom")
|
||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "oldcontent"))
|
||||
hb.writeElementClose("strong")
|
||||
return hb.html()
|
||||
}
|
||||
|
||||
// Render the HTML to show interactions
|
||||
func (a *goBlog) renderInteractions(b *configBlog, canonical string) template.HTML {
|
||||
if b == nil || canonical == "" {
|
||||
return ""
|
||||
}
|
||||
var hb htmlBuilder
|
||||
// Start accordion
|
||||
hb.writeElementOpen("details", "class", "p", "id", "interactions")
|
||||
hb.writeElementOpen("summary")
|
||||
hb.writeElementOpen("strong")
|
||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "interactions"))
|
||||
hb.writeElementClose("strong")
|
||||
hb.writeElementClose("summary")
|
||||
// Render mentions
|
||||
var renderMentions func(m []*mention)
|
||||
renderMentions = func(m []*mention) {
|
||||
if len(m) == 0 {
|
||||
return
|
||||
}
|
||||
hb.writeElementOpen("ul")
|
||||
for _, mention := range m {
|
||||
hb.writeElementOpen("li")
|
||||
hb.writeElementOpen("a", "href", mention.Url, "target", "_blank", "rel", "nofollow noopener noreferrer ugc")
|
||||
hb.writeEscaped(defaultIfEmpty(mention.Author, mention.Url))
|
||||
hb.writeElementClose("a")
|
||||
if mention.Title != "" {
|
||||
hb.write(" ")
|
||||
hb.writeElementOpen("strong")
|
||||
hb.writeEscaped(mention.Title)
|
||||
hb.writeElementClose("strong")
|
||||
}
|
||||
if mention.Content != "" {
|
||||
hb.write(" ")
|
||||
hb.writeElementOpen("i")
|
||||
hb.writeEscaped(mention.Content)
|
||||
hb.writeElementClose("i")
|
||||
}
|
||||
if len(mention.Submentions) > 0 {
|
||||
renderMentions(mention.Submentions)
|
||||
}
|
||||
hb.writeElementClose("li")
|
||||
}
|
||||
hb.writeElementClose("ul")
|
||||
}
|
||||
renderMentions(a.db.getWebmentionsByAddress(canonical))
|
||||
// Show form to send a webmention
|
||||
hb.writeElementOpen("form", "class", "fw p", "method", "post", "action", "/webmention")
|
||||
hb.writeElementOpen("label", "for", "wm-source", "class", "p")
|
||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "interactionslabel"))
|
||||
hb.writeElementClose("label")
|
||||
hb.writeElementOpen("input", "id", "wm-source", "type", "url", "name", "source", "placeholder", "URL", "required", "")
|
||||
hb.writeElementOpen("input", "type", "hidden", "name", "target", "value", canonical)
|
||||
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(b.Lang, "send"))
|
||||
hb.writeElementClose("form")
|
||||
// Show form to create a new comment
|
||||
hb.writeElementOpen("form", "class", "fw p", "method", "post", "action", "/comment")
|
||||
hb.writeElementOpen("input", "type", "hidden", "name", "target", "value", canonical)
|
||||
hb.writeElementOpen("input", "type", "text", "name", "name", "placeholder", a.ts.GetTemplateStringVariant(b.Lang, "nameopt"))
|
||||
hb.writeElementOpen("input", "type", "url", "name", "website", "placeholder", a.ts.GetTemplateStringVariant(b.Lang, "websiteopt"))
|
||||
hb.writeElementOpen("textarea", "name", "comment", "required", "", "placeholder", a.ts.GetTemplateStringVariant(b.Lang, "comment"))
|
||||
hb.writeElementClose("textarea")
|
||||
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(b.Lang, "docomment"))
|
||||
hb.writeElementClose("form")
|
||||
// Finish accordion
|
||||
hb.writeElementClose("details")
|
||||
return hb.html()
|
||||
}
|
||||
|
||||
// Render HTML for author h-card
|
||||
func (a *goBlog) renderAuthor() template.HTML {
|
||||
user := a.cfg.User
|
||||
if user == nil {
|
||||
return ""
|
||||
}
|
||||
var hb htmlBuilder
|
||||
hb.writeElementOpen("div", "class", "p-author h-card hide")
|
||||
if user.Picture != "" {
|
||||
hb.writeElementOpen("data", "class", "u-photo", "value", user.Picture)
|
||||
hb.writeElementClose("data")
|
||||
}
|
||||
if user.Name != "" {
|
||||
hb.writeElementOpen("a", "class", "p-name u-url", "rel", "me", "href", defaultIfEmpty(user.Link, "/"))
|
||||
hb.writeEscaped(user.Name)
|
||||
hb.writeElementClose("a")
|
||||
}
|
||||
hb.writeElementClose("div")
|
||||
return hb.html()
|
||||
}
|
||||
|
||||
// Render HTML for TOR notice in the footer
|
||||
func (a *goBlog) renderTorNotice(b *configBlog, torUsed bool, torAddress string) template.HTML {
|
||||
if !a.cfg.Server.Tor || b == nil || !torUsed && torAddress == "" {
|
||||
return ""
|
||||
}
|
||||
var hb htmlBuilder
|
||||
if torUsed {
|
||||
hb.writeElementOpen("p", "id", "tor")
|
||||
hb.writeEscaped("🔐 ")
|
||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "connectedviator"))
|
||||
hb.writeElementClose("p")
|
||||
} else if torAddress != "" {
|
||||
hb.writeElementOpen("p", "id", "tor")
|
||||
hb.writeEscaped("🔓 ")
|
||||
hb.writeElementOpen("a", "href", torAddress)
|
||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "connectviator"))
|
||||
hb.writeElementClose("a")
|
||||
hb.writeEscaped(" ")
|
||||
hb.writeElementOpen("a", "href", "https://www.torproject.org/", "target", "_blank", "rel", "nofollow noopener noreferrer")
|
||||
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "whatistor"))
|
||||
hb.writeElementClose("a")
|
||||
hb.writeElementClose("p")
|
||||
}
|
||||
return hb.html()
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_renderPostTax(t *testing.T) {
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
_ = app.initConfig()
|
||||
_ = app.initDatabase(false)
|
||||
app.initComponents(false)
|
||||
|
||||
p := &post{
|
||||
Parameters: map[string][]string{
|
||||
"tags": {"Foo", "Bar"},
|
||||
},
|
||||
}
|
||||
|
||||
res := app.renderPostTax(p, app.cfg.Blogs["default"])
|
||||
_, err := goquery.NewDocumentFromReader(strings.NewReader(string(res)))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, template.HTML("<p><strong>Tags</strong>: <a class=\"p-category\" rel=\"tag\" href=\"/tags/bar\">Bar</a>, <a class=\"p-category\" rel=\"tag\" href=\"/tags/foo\">Foo</a></p>"), res)
|
||||
}
|
||||
|
||||
func Test_renderOldContentWarning(t *testing.T) {
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
_ = app.initConfig()
|
||||
_ = app.initDatabase(false)
|
||||
app.initComponents(false)
|
||||
|
||||
p := &post{
|
||||
Published: "2018-01-01",
|
||||
}
|
||||
|
||||
res := app.renderOldContentWarning(p, app.cfg.Blogs["default"])
|
||||
_, err := goquery.NewDocumentFromReader(strings.NewReader(string(res)))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, template.HTML("<strong class=\"p border-top border-bottom\">⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed.</strong>"), res)
|
||||
}
|
||||
|
||||
func Test_renderInteractions(t *testing.T) {
|
||||
var err error
|
||||
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
app.cfg.Server.PublicAddress = "https://example.com"
|
||||
_ = app.initConfig()
|
||||
_ = app.initDatabase(false)
|
||||
app.initComponents(false)
|
||||
app.d, err = app.buildRouter()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = app.createPost(&post{
|
||||
Path: "/testpost1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = app.createPost(&post{
|
||||
Path: "/testpost2",
|
||||
Content: "[Test](/testpost1)",
|
||||
Parameters: map[string][]string{
|
||||
"title": {"Test-Title"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = app.verifyMention(&mention{
|
||||
Source: "https://example.com/testpost2",
|
||||
Target: "https://example.com/testpost1",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = app.db.approveWebmentionId(1)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = app.createPost(&post{
|
||||
Path: "/testpost3",
|
||||
Content: "[Test](/testpost2)",
|
||||
Parameters: map[string][]string{
|
||||
"title": {"Test-Title"},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = app.verifyMention(&mention{
|
||||
Source: "https://example.com/testpost3",
|
||||
Target: "https://example.com/testpost2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = app.db.approveWebmentionId(2)
|
||||
require.NoError(t, err)
|
||||
|
||||
res := app.renderInteractions(app.cfg.Blogs["default"], "https://example.com/testpost1")
|
||||
_, err = goquery.NewDocumentFromReader(strings.NewReader(string(res)))
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := os.ReadFile("testdata/interactionstest.html")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, template.HTML(expected), res)
|
||||
}
|
||||
|
||||
func Test_renderAuthor(t *testing.T) {
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
app.cfg.User.Picture = "https://example.com/picture.jpg"
|
||||
app.cfg.User.Name = "John Doe"
|
||||
_ = app.initConfig()
|
||||
_ = app.initDatabase(false)
|
||||
app.initComponents(false)
|
||||
|
||||
res := app.renderAuthor()
|
||||
_, err := goquery.NewDocumentFromReader(strings.NewReader(string(res)))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, template.HTML("<div class=\"p-author h-card hide\"><data class=\"u-photo\" value=\"https://example.com/picture.jpg\"></data><a class=\"p-name u-url\" rel=\"me\" href=\"/\">John Doe</a></div>"), res)
|
||||
}
|
||||
|
||||
func Test_renderTorNotice(t *testing.T) {
|
||||
app := &goBlog{
|
||||
cfg: createDefaultTestConfig(t),
|
||||
}
|
||||
_ = app.initConfig()
|
||||
_ = app.initDatabase(false)
|
||||
app.initComponents(false)
|
||||
|
||||
app.cfg.Server.Tor = true
|
||||
|
||||
res := app.renderTorNotice(app.cfg.Blogs["default"], true, "http://abc.onion:80/test")
|
||||
_, err := goquery.NewDocumentFromReader(strings.NewReader(string(res)))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, template.HTML("<p id=\"tor\">🔐 Connected via Tor.</p>"), res)
|
||||
|
||||
res = app.renderTorNotice(app.cfg.Blogs["default"], false, "http://abc.onion:80/test")
|
||||
_, err = goquery.NewDocumentFromReader(strings.NewReader(string(res)))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, template.HTML("<p id=\"tor\">🔓 <a href=\"http://abc.onion:80/test\">Connect via Tor.</a> <a href=\"https://www.torproject.org/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">What is Tor?</a></p>"), res)
|
||||
}
|
Loading…
Reference in New Issue