From 1ce3d635f0b42b6b8ae6b9dca98846695d8ceba2 Mon Sep 17 00:00:00 2001 From: Razon Yang Date: Thu, 22 Sep 2022 08:44:11 +0800 Subject: [PATCH] feat: add support for Staticman comments (#725) * feat: add support for Staticman comments * chore(staticman): replace baseURL with endpoint, and add the service parameter to use v3 API * i18n(staticman): add translations * feat(staticman): format comment date distance from now * chore: clean up * feat(staticman): add support for reCaptcha v2 * feat(staticman): add extra fields * fix(staticman): avoid submitting an invalid form * fix(staticman): filter invalid message * fix(staticman): verify reCaptcha * style(table): add gaps between responsive table and others * docs: document the Staticman comments engine * docs: update Staticman advantages and disadvantages * chore: update backers Add Wilson E. Alvarez as a backer * docs: update Staticman advantages * feat(staticman): paginate comments pages * docs: document the staticman.paginate parameter * feat(staticman): add the staticman.requiredFields parameter * chore(staticman): use the mystery-person image as the default avatar * feat(staticman): add the staticman.moderation parameter * docs: clean up * style: change the comment name color * chore: show comments pagination when total pages greater than 1 * fix(staticman): save comments to data folder * fix(staticman): correct comments pages title * fix(staticman): reset reCaptcha after submitting * fix(static): do not reset submit button after submitting when recaptcha is enabled * fix(staticman): do not disabled submit button if reCaptcha is disabled * chore(staticman): display the API error message if possible * docs: update Staticman pages * fix(staticman): adjust reCaptcha theme on first load * style(staticman): change fields width as full width on small screens * docs: fix requiredFields and empty siteKey for staticman.yml Co-authored-by: Wilson E. Alvarez --- README.md | 1 + assets/icons/index.js | 100 ++++++----- assets/js/staticman/client.ts | 87 ++++++++++ assets/js/staticman/date-renderer.ts | 45 +++++ assets/js/staticman/form.ts | 67 ++++++++ assets/js/staticman/index.ts | 18 ++ assets/js/staticman/reply.ts | 54 ++++++ assets/main/scss/_form.scss | 10 ++ assets/main/scss/_table.scss | 4 + assets/main/scss/index.scss | 1 + assets/templates/staticman/comments.html | 36 ++++ exampleSite/config/_default/params.toml | 14 ++ .../getting-started/introduction/index.md | 2 +- .../introduction/index.zh-hans.md | 2 +- .../introduction/index.zh-hant.md | 2 +- .../content/docs/widgets/comments/index.md | 157 +++++++++++++++++- .../docs/widgets/comments/index.zh-hans.md | 157 +++++++++++++++++- .../docs/widgets/comments/index.zh-hant.md | 157 +++++++++++++++++- i18n/en.toml | 21 +++ i18n/zh-cn.toml | 21 +++ i18n/zh-hans.toml | 21 +++ i18n/zh-hant.toml | 21 +++ i18n/zh-hk.toml | 21 +++ i18n/zh-tw.toml | 21 +++ layouts/partials/assets/main/js.html | 3 + layouts/partials/post/comments.html | 1 + layouts/partials/post/comments/staticman.html | 73 ++++++++ layouts/partials/staticman/comments.html | 36 ++++ layouts/partials/staticman/form-body.html | 42 +++++ layouts/partials/staticman/form.html | 3 + layouts/partials/staticman/list-item.html | 44 +++++ layouts/partials/staticman/list.html | 17 ++ layouts/partials/staticman/pagination.html | 35 ++++ layouts/partials/staticman/recaptcha.html | 27 +++ layouts/partials/staticman/reply-form.html | 3 + layouts/partials/staticman/reply-modal.html | 15 ++ package.hugo.json | 1 + 37 files changed, 1281 insertions(+), 59 deletions(-) create mode 100644 assets/js/staticman/client.ts create mode 100644 assets/js/staticman/date-renderer.ts create mode 100644 assets/js/staticman/form.ts create mode 100644 assets/js/staticman/index.ts create mode 100644 assets/js/staticman/reply.ts create mode 100644 assets/main/scss/_form.scss create mode 100644 assets/templates/staticman/comments.html create mode 100644 layouts/partials/post/comments/staticman.html create mode 100644 layouts/partials/staticman/comments.html create mode 100644 layouts/partials/staticman/form-body.html create mode 100644 layouts/partials/staticman/form.html create mode 100644 layouts/partials/staticman/list-item.html create mode 100644 layouts/partials/staticman/list.html create mode 100644 layouts/partials/staticman/pagination.html create mode 100644 layouts/partials/staticman/recaptcha.html create mode 100644 layouts/partials/staticman/reply-form.html create mode 100644 layouts/partials/staticman/reply-modal.html diff --git a/README.md b/README.md index f153fb5ac..750efadf7 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,4 @@ Sponsors will appear on our website with their brands and logos. - [Arvind Singh](https://github.com/ArvindRSingh) - [Vri](https://github.com/vrifox) - [Natenom](https://github.com/Natenom) +- [Wilson E. Alvarez](https://github.com/Rubonnek) diff --git a/assets/icons/index.js b/assets/icons/index.js index 6b66891c7..f581e45dc 100644 --- a/assets/icons/index.js +++ b/assets/icons/index.js @@ -1,13 +1,50 @@ import { config, dom, library } from '@fortawesome/fontawesome-svg-core'; +import { + faAlipay, + faArtstation, + faBitbucket, + faCreativeCommons, + faCreativeCommonsBy, + faCreativeCommonsNc, + faCreativeCommonsNd, + faCreativeCommonsSa, + faDiscord, + faDiscourse, + faDocker, + faFacebookF, + faFacebookSquare, + faGithub, + faGitlab, + faInstagram, + faKaggle, + faLastfm, + faLinkedinIn, + faMastodon, + faMediumM, + faPatreon, + faPaypal, + faPinterest, + faQq, + faQuora, + faReddit, + faStackOverflow, + faTelegramPlane, + faTiktok, + faTumblr, + faTwitter, + faWeibo, + faWeixin, + faXing, + faYoutube, + faZhihu +} from '@fortawesome/free-brands-svg-icons'; +import { faStar } from '@fortawesome/free-regular-svg-icons'; import { faAdjust, faArrowsAltV, faBars, faBuilding, - faCheckCircle, - faChevronDown, - faChevronCircleDown, - faCode, + faCheckCircle, faChevronCircleDown, faChevronDown, faCode, faCoffee, faCog, faColumns, @@ -24,11 +61,8 @@ import { faFileAlt, faFileArchive, faFolder, - faFont, - faHistory, - faHome, - faGlobe, - faInfoCircle, + faFont, faGlobe, faHistory, + faHome, faInfoCircle, faLanguage, faLink, faList, @@ -38,8 +72,7 @@ import { faPalette, faQuestion, faQuestionCircle, - faRedoAlt, - faRss, + faRedoAlt, faReply, faRss, faSearch, faShareAlt, faSkullCrossbones, @@ -51,50 +84,10 @@ import { faTimes, faTv, faUndo, - faUser, + faUser } from '@fortawesome/free-solid-svg-icons'; -import { faStar } from '@fortawesome/free-regular-svg-icons'; -import { - faAlipay, - faArtstation, - faBitbucket, - faCreativeCommons, - faCreativeCommonsBy, - faCreativeCommonsNc, - faCreativeCommonsNd, - faCreativeCommonsSa, - faDiscord, - faDiscourse, - faDocker, - faFacebookF, - faFacebookSquare, - faGithub, - faGitlab, - faInstagram, - faKaggle, - faLastfm, - faLinkedinIn, - faMastodon, - faMediumM, - faPatreon, - faPaypal, - faPinterest, - faQq, - faQuora, - faReddit, - faStackOverflow, - faTelegramPlane, - faTiktok, - faTumblr, - faTwitter, - faWeibo, - faWeixin, - faXing, - faYoutube, - faZhihu, -} from '@fortawesome/free-brands-svg-icons'; -import { faBilibili, faLiberapay, faOffline, faTipeee } from './icons'; import { default as customIcons } from './custom'; +import { faBilibili, faLiberapay, faOffline, faTipeee } from './icons'; let icons = [ // Solid Icons @@ -134,6 +127,7 @@ let icons = [ faQuestion, faQuestionCircle, faRedoAlt, + faReply, faRss, faSearch, faShareAlt, diff --git a/assets/js/staticman/client.ts b/assets/js/staticman/client.ts new file mode 100644 index 000000000..4335384fe --- /dev/null +++ b/assets/js/staticman/client.ts @@ -0,0 +1,87 @@ +import { default as params } from '@params'; +import snackbar from 'js/snackbar'; + +class Client +{ + private apiUrl: string; + + private reCaptchaKey = ''; + private reCaptchaSecret = ''; + + private moderation: boolean; + + constructor() + { + const endpoint = params.staticman.endpoint.replace(/\/*$/, "") + const service = params.staticman.service ? params.staticman.service : 'github' + const repo = params.staticman.repo + const branch = params.staticman.branch ? params.staticman.branch : 'master' + const property = params.staticman.property ? params.staticman.property : 'comments' + this.reCaptchaKey = params.staticman.recaptchakey ? params.staticman.recaptchakey : ''; + this.reCaptchaSecret = params.staticman.recaptchasecret ? params.staticman.recaptchasecret : ''; + this.moderation = "moderation" in params.staticman ? params.staticman.moderation : true; + this.apiUrl = `${endpoint}/v3/entry/${service}/${repo}/${branch}/${property}` + } + + send(form: FormData) { + const reCaptchaToken = form.get('reCaptchaToken') + if (this.reCaptchaKey && !reCaptchaToken) { + throw new Error('reCaptcha token missing') + } + + let slug = form.get('slug') + + const data = { + 'g-recaptcha-response': reCaptchaToken, + options: { + slug: slug, + reCaptcha: { + siteKey: this.reCaptchaKey, + secret: this.reCaptchaSecret, + }, + }, + fields: { + reply_to: form.get('reply_to'), + root_id: form.get('root_id'), + name: form.get('name'), + email: form.get('email'), + message: form.get('message'), + url: form.get('url'), + }, + } + + return fetch(this.apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data), + }).then((response) => { + return response.json(); + }).then((data) => { + if (data.success === true) { + snackbar.show(this.sucessMessage(), 5000); + } else if (data.message) { + snackbar.show(data.message); + } else { + snackbar.show(data.error.text); + } + }).catch((err) => { + console.error(err) + snackbar.show('Comment failed.'); + }) + } + + sucessMessage(): string + { + if (this.moderation) { + return 'Comment successfully submitted! Your message will be shown once our moderators review it.'; + } + + return 'Comment successfully submitted! Your message will be shown in a few minutes.'; + } +} + +const client = new Client(); + +export default client; diff --git a/assets/js/staticman/date-renderer.ts b/assets/js/staticman/date-renderer.ts new file mode 100644 index 000000000..b977217c6 --- /dev/null +++ b/assets/js/staticman/date-renderer.ts @@ -0,0 +1,45 @@ +import { formatDistance } from 'date-fns'; +import { enUS, zhCN, zhTW } from 'date-fns/locale'; + +const locales = { + 'en': enUS, + 'en-us': enUS, + 'zh-cn': zhCN, + 'zh-hans': zhCN, + 'zh-tw': zhTW, + 'zh-hk': zhTW, + 'zh-hant': zhTW, +}; + +class DateRenderer +{ + private items: Array; + private lang: string; + + constructor(items: string) + { + this.items = Array.from(document.querySelectorAll(items)) as Array + this.lang = document.documentElement.getAttribute('lang').toLowerCase(); + } + + run() + { + if (!this.items) { + return + } + + const now = new Date(); + this.items.forEach((item: HTMLElement) => { + const timestamp = parseInt(item.getAttribute('data-timestamp')); + const date = new Date(timestamp * 1000); + item.innerText = formatDistance(now, date, { addSuffix: true, locale: this.getLocale() }) + }) + } + + getLocale() + { + return this.lang in locales ? locales[this.lang] : enUS; + } +} + +export default DateRenderer; diff --git a/assets/js/staticman/form.ts b/assets/js/staticman/form.ts new file mode 100644 index 000000000..dcdbe6a01 --- /dev/null +++ b/assets/js/staticman/form.ts @@ -0,0 +1,67 @@ +import Component from "js/component"; + +abstract class Form implements Component +{ + protected form: HTMLFormElement; + + private btnSubmit: HTMLButtonElement; + + private locked = false; + + constructor(form: string) + { + this.form = document.getElementById(form) as HTMLFormElement + } + + run() { + if (!this.form) { + return; + } + + this.btnSubmit = this.form.querySelector('button[type="submit"]'); + + this.form.addEventListener('submit', (e) => { + e.preventDefault() + if (!this.form.checkValidity()) { + e.stopPropagation() + return; + } + + if (this.locked !== false) { + return; + } + + this.lock(); + + this.submit(new FormData(this.form)).finally(() => { + this.unlock(); + this.reset(); + }); + }) + } + + abstract submit(data: FormData): Promise; + + lock() { + this.locked = true + this.btnSubmit.setAttribute('disabled', 'true'); + } + + unlock() { + this.locked = false + if (!this.form.hasAttribute('data-g-recaptcha-id')) { + this.btnSubmit.removeAttribute('disabled'); + } + } + + reset() { + this.form.reset(); + + const reCaptchaWidgetId = this.form.getAttribute('data-g-recaptcha-id'); + if (reCaptchaWidgetId) { + window.grecaptcha.reset(reCaptchaWidgetId) + } + } +} + +export default Form; diff --git a/assets/js/staticman/index.ts b/assets/js/staticman/index.ts new file mode 100644 index 000000000..70232e64b --- /dev/null +++ b/assets/js/staticman/index.ts @@ -0,0 +1,18 @@ +import client from "js/staticman/client"; +import DateRenderer from "js/staticman/date-renderer"; +import Form from "js/staticman/form"; +import Reply from "js/staticman/reply"; + +class Staticman extends Form +{ + submit(data: FormData): Promise + { + return client.send(data); + } +} + +(new Staticman('staticman-comment-form')).run(); + +(new Reply('staticman-reply-form', '.staticman-reply-button')).run(); + +(new DateRenderer('.staticman-comment-date')).run(); diff --git a/assets/js/staticman/reply.ts b/assets/js/staticman/reply.ts new file mode 100644 index 000000000..51a164608 --- /dev/null +++ b/assets/js/staticman/reply.ts @@ -0,0 +1,54 @@ +import client from "js/staticman/client"; +import Form from "js/staticman/form"; + +class Reply extends Form +{ + private buttons: Array; + + private modalCloseBtn: HTMLButtonElement + + constructor(form: string, buttons: string) + { + super(form); + this.buttons = Array.from(document.querySelectorAll(buttons)) as Array; + this.modalCloseBtn = document.querySelector('#comment-reply-modal .btn-close') as HTMLButtonElement; + } + + run() + { + super.run(); + + if (!this.form) { + return + } + + const replyTo: HTMLInputElement = this.form.querySelector('input[name="reply_to"]'); + const rootId: HTMLInputElement = this.form.querySelector('input[name="root_id"]'); + + this.buttons.forEach((button) => { + button.addEventListener('click', (e) => { + document.getElementById('comment-reply-modal-to').innerText = '@' + button.getAttribute('data-comment-name'); + rootId.value = button.getAttribute('data-root-id'); + replyTo.value = button.getAttribute('data-comment-id'); + }); + }) + } + + reset(): void { + super.reset(); + + this.modalCloseBtn.click(); + } + + submit(data: FormData): Promise { + const rootId = data.get('root_id'); + const replyTo = data.get('reply_to'); + if (!rootId || !replyTo) { + throw new Error('invalid action'); + } + + return client.send(data) + } +} + +export default Reply; diff --git a/assets/main/scss/_form.scss b/assets/main/scss/_form.scss new file mode 100644 index 000000000..82a6b4fea --- /dev/null +++ b/assets/main/scss/_form.scss @@ -0,0 +1,10 @@ +.form-floating { + &.required { + label { + &:after { + content: "*"; + color: red; + } + } + } +} diff --git a/assets/main/scss/_table.scss b/assets/main/scss/_table.scss index 7b533cba5..6b4a8b5e9 100644 --- a/assets/main/scss/_table.scss +++ b/assets/main/scss/_table.scss @@ -7,3 +7,7 @@ table { @extend .table-hover; /* stylelint-disable-line */ } } + +.table-responsive { + margin-bottom: 1rem; +} diff --git a/assets/main/scss/index.scss b/assets/main/scss/index.scss index afc2780b2..1d5f84d71 100644 --- a/assets/main/scss/index.scss +++ b/assets/main/scss/index.scss @@ -15,6 +15,7 @@ @import 'component'; @import 'code'; @import 'dropdown'; +@import 'form'; @import 'gallery'; @import 'highlight'; @import 'icons'; diff --git a/assets/templates/staticman/comments.html b/assets/templates/staticman/comments.html new file mode 100644 index 000000000..bd49284cf --- /dev/null +++ b/assets/templates/staticman/comments.html @@ -0,0 +1,36 @@ + +{{- $color := default "auto" $.Site.Params.color }} +{{- if not (in (slice "light" "dark" "auto") $color) }} + {{- $color = "auto" }} +{{- end }} + + + {{- partial "assets/init/js" . -}} + + + {{- $currentPage := .Scratch.Get "commentsPage" }} + {{- $separator := .Site.Params.TitleSeparator }} + {{ printf "%s %s %s %s " (i18n "comments") $separator (i18n "pagination_title" (dict "PageNumber" $currentPage)) $separator }}{{- partial "head/title" . -}} + {{- partial "head.html" . -}} + {{- block "head-end" . -}}{{- end -}} + {{- partial "hooks/head-end" . -}} + + + {{- partial "header.html" . }} + {{- partial "hooks/main-begin" . }} +
+
+ {{- partialCached "noscript" . .Language.Lang }} + {{- partial "breadcrumb" . }} + {{- partial "staticman/comments" . }} +
+
+ {{- partial "hooks/main-end" . }} + {{- partial "footer.html" . }} + {{- partial "body-end" . }} + + + diff --git a/exampleSite/config/_default/params.toml b/exampleSite/config/_default/params.toml index d8ec0ef84..439eaff94 100644 --- a/exampleSite/config/_default/params.toml +++ b/exampleSite/config/_default/params.toml @@ -140,6 +140,20 @@ viewer = true # Image Viewer # Optional values: github-light, github-dark, preferred-color-scheme, github-dark-orange, icy-dark, dark-blue, photon-dark. # theme = "" +# [staticman] + # endpoint = "https://yourstaticmaninstance" # Required, replace with your own Staticman instance. + # repo = "user/repo" # Required + # service = "github" # Default to GitHub. + # branch = "main" # Default to master. + # property = "comments" # Default to comments. + # sorting = "desc" # Default to asc. + # reCaptchaKey = "" # The reCaptcha site key. + # reCaptchaSecret = "" # The reCaptcha encrypted secret. You'll need to encrypt plain secret via https://yourstaticmaninstance/v3/encrypt/PLAINSECRET. + # extraFields = [ "url" ] # Extra fields. Available fiedls: url. + # requiredFields = [ "email", "url" ] # Extra required fields. Available fields: email and the extra fields. + # paginate = 5 # Default to 10. + # moderation = false # Same as Staticman's moderation. + [giscus] repo = "razonyang/hugo-theme-bootstrap" repoId = "MDEwOlJlcG9zaXRvcnkzMDQzMzI4NTM=" diff --git a/exampleSite/content/docs/getting-started/introduction/index.md b/exampleSite/content/docs/getting-started/introduction/index.md index b20cf46c7..0c2a99181 100644 --- a/exampleSite/content/docs/getting-started/introduction/index.md +++ b/exampleSite/content/docs/getting-started/introduction/index.md @@ -59,7 +59,7 @@ Hugo Bootstrap Theme(HBS) is a fast, responsive, multipurpose and feature-rich H - [Syntax Highlighting]({{< ref "/docs/look-and-feel#syntax-highlighting" >}}). - [Reward Widget]({{< ref "/docs/widgets/reward" >}}), AKA Buy Me a Coffee: any platform that supports QR code is supported. - Table of Contents. -- [Comments]({{< ref "/docs/widgets/comments" >}}): Disqus, Utterances and Giscus can be used out of box, you can also integrate with other comments services. +- [Comments]({{< ref "/docs/widgets/comments" >}}): Disqus, Utterances, Giscus and Staticman can be used out of box, you can also integrate with other comments services. - [Custom Assets]({{< ref "/docs/advanced/custom-assets" >}}): custom CSS and JS. - [Hooks]({{< ref "/docs/advanced/hooks" >}}): provides ability to add custom code on page. - [Social Links]({{< ref "/docs/widgets/social-links" >}}). diff --git a/exampleSite/content/docs/getting-started/introduction/index.zh-hans.md b/exampleSite/content/docs/getting-started/introduction/index.zh-hans.md index 23f264856..bdf5a4565 100644 --- a/exampleSite/content/docs/getting-started/introduction/index.zh-hans.md +++ b/exampleSite/content/docs/getting-started/introduction/index.zh-hans.md @@ -59,7 +59,7 @@ Hugo Bootstrap Theme(HBS) 是一个快速、响应式、多用途和功能丰富 - [Syntax Highlighting]({{< ref "/docs/look-and-feel#syntax-highlighting" >}}). - [Reward Widget]({{< ref "/docs/widgets/reward" >}}), AKA Buy Me a Coffee: any platform that supports QR code is supported. - Table of Contents. -- [Comments]({{< ref "/docs/widgets/comments" >}}): Disqus, Utterances and Giscus can be used out of box, you can also integrate with other comments services. +- [Comments]({{< ref "/docs/widgets/comments" >}}): Disqus, Utterances, Giscus and Staticman can be used out of box, you can also integrate with other comments services. - [Custom Assets]({{< ref "/docs/advanced/custom-assets" >}}): custom CSS and JS. - [Hooks]({{< ref "/docs/advanced/hooks" >}}): provides ability to add custom code on page. - [Social Links]({{< ref "/docs/widgets/social-links" >}}). diff --git a/exampleSite/content/docs/getting-started/introduction/index.zh-hant.md b/exampleSite/content/docs/getting-started/introduction/index.zh-hant.md index ccb8a1f78..583b19af3 100644 --- a/exampleSite/content/docs/getting-started/introduction/index.zh-hant.md +++ b/exampleSite/content/docs/getting-started/introduction/index.zh-hant.md @@ -59,7 +59,7 @@ Hugo Bootstrap Theme(HBS) 是壹個快速、響應式、多用途和功能豐富 - [Syntax Highlighting]({{< ref "/docs/look-and-feel#syntax-highlighting" >}}). - [Reward Widget]({{< ref "/docs/widgets/reward" >}}), AKA Buy Me a Coffee: any platform that supports QR code is supported. - Table of Contents. -- [Comments]({{< ref "/docs/widgets/comments" >}}): Disqus, Utterances and Giscus can be used out of box, you can also integrate with other comments services. +- [Comments]({{< ref "/docs/widgets/comments" >}}): Disqus, Utterances, Giscus and Staticman can be used out of box, you can also integrate with other comments services. - [Custom Assets]({{< ref "/docs/advanced/custom-assets" >}}): custom CSS and JS. - [Hooks]({{< ref "/docs/advanced/hooks" >}}): provides ability to add custom code on page. - [Social Links]({{< ref "/docs/widgets/social-links" >}}). diff --git a/exampleSite/content/docs/widgets/comments/index.md b/exampleSite/content/docs/widgets/comments/index.md index d4e8a7d0f..96bfb31d4 100644 --- a/exampleSite/content/docs/widgets/comments/index.md +++ b/exampleSite/content/docs/widgets/comments/index.md @@ -12,7 +12,8 @@ categories = [ tags = [ "Disqus", "Utterances", - "Giscus" + "Giscus", + "Staticman" ] series = [ "Docs" @@ -100,6 +101,160 @@ disqusShortname = "yourdiscussshortname" | `giscus.lang` | String | - | Specify language, default to site language. | `giscus.lazyLoading` | Boolean | `true` | Enable lazy loading. +## Staticman + +[Staticman](https://staticman.net/) is also supported out of box, but it may be too complex to set it up. + +### Advantages + +- The comments files are stored inside your repository, that is, the comments are rendered during the site build. It maybe useful for SEO, since it doesn't rely JS to load data dynamically, it's truly static. +- Handling spam: approve or reject comment via Git provider pull request, Google reCaptcha. +- Native theme style. + +### Disadvantages + +- The user information are untrusted, such as email. +- User unable to delete their comments directly, it's able to do that via Pull Request. + +### Preparations + +#### Staticman instance + +Firstly, we should set up a Staticman instance for handling comments requests. + +##### Self-Hosted Staticman instance + +Please see https://staticman.net/docs/getting-started.html#step-2-deploy-staticman for details. + +##### Public Staticman instance + +I set up a public staticman instance for testing, it should works with GitHub repositories only. + +> You'll need to install the [GitHub App](https://github.com/apps/hbs-staticman) for your repo, so that the instance has access to write comments files to your repo. + +- Endpoint: https://hbs-staticman.herokuapp.com + +#### Staticman Configuration + +Secondary, we need to create the `staticman.yml` under your site/repository root, so that Staticman instance can read the configuration and process comments requests. + +{{% alert "warning" %}} +The `filename`, `path` are fixed, please **DO NOT** modify those parameters. +{{% /alert %}} + +{{% alert "warning" %}} +The `allowedFields` MUST include `name`, `email`, `message`, `reply_to`, `root_id` fields. +{{% /alert %}} + +```yaml +# Name of the property. You can have multiple properties with completely +# different config blocks for different sections of your site. +# For example, you can have one property to handle comment submission and +# another one to handle posts. +comments: + # (*) REQUIRED + # + # Names of the fields the form is allowed to submit. If a field that is + # not here is part of the request, an error will be thrown. + allowedFields: ["name", "email", "url", "message", "reply_to", "root_id"] + + # (*) REQUIRED + # + # Name of the branch being used. Must match the one sent in the URL of the + # request. + branch: "main" + + # Text to use as the commit message or pull request title. Accepts placeholders. + commitMessage: "Add Staticman comment" + + # (*) REQUIRED + # + # Destination path (filename) for the data files. Accepts placeholders. + filename: "{@id}" # DO NOT MODIFY + + # The format of the generated data files. Accepted values are "json", "yaml" + # or "frontmatter" + format: "yaml" + + # List of fields to be populated automatically by Staticman and included in + # the data file. Keys are the name of the field. The value can be an object + # with a `type` property, which configures the generated field, or any value + # to be used directly (e.g. a string, number or array) + generatedFields: + date: + type: date + options: + format: "timestamp-seconds" + + # Whether entries need to be appproved before they are published to the main + # branch. If set to `true`, a pull request will be created for your approval. + # Otherwise, entries will be published to the main branch automatically. + moderation: false + + # Name of the site. Used in notification emails. + name: "hbs.razonyang.com" + + # Notification settings. When enabled, users can choose to receive notifications + # via email when someone adds a reply or a new comment. This requires an account + # with Mailgun, which you can get for free at http://mailgun.com. + #notifications: + # Enable notifications + #enabled: true + + # (!) ENCRYPTED + # + # Mailgun API key + #apiKey: "1q2w3e4r" + + # (!) ENCRYPTED + # + # Mailgun domain (encrypted) + #domain: "4r3e2w1q" + + # (*) REQUIRED + # + # Destination path (directory) for the data files. Accepts placeholders. + path: "data/{options.slug}" # DO NOT MODIFY + + # Names of required fields. If any of these isn't in the request or is empty, + # an error will be thrown. + requiredFields: ["name", "message"] + + # List of transformations to apply to any of the fields supplied. Keys are + # the name of the field and values are possible transformation types. + transforms: + email: md5 # DO NOT MODIFY, REQUIRED BY AVATAR. + + reCaptcha: + enabled: false + siteKey: "" + secret: +``` + +#### Site Parameters + +Finally, tweak the following `staticman` parameters in `params.toml`. + +| Name | Type | Default | Description | +|:---|:---|:--:|:--- +| `staticman` | Object | | +| `staticman.endpoint` | String | - | THe Staticman instance endpoint. Required. +| `staticman.repo` | String | - | Repository that with form `user/repo`. Required. +| `staticman.service` | String | `github` | +| `staticman.branch` | String | `master` | Repository branch. +| `staticman.property` | String | `comments` | +| `staticman.sorting` | String | `asc` | Sorting comments, available options: `desc`. +| `staticman.reCaptchaKey` | String | `-` | The reCaptcha site key. +| `staticman.reCaptchaSecret` | String | `-` | The reCaptcha encrypted secret. You'll need to encrypt plain secret via https://yourstaticmaninstance/v3/encrypt/plainsecret. +| `staticman.extraFields` | Array | `-` | Extra fields. Available fiedls: `url`. +| `staticman.requiredFields` | Array | `-` | Extra required fields. Available fields: `email` and the extra fields. `name` and `message` are always required. +| `staticman.paginate` | Integer | `10` | The number of comments per page. +| `staticman.moderation` | Boolean | `true` | Same as Staticman's moderation. + +#### reCaptcha + +The reCaptcha secret is not the plain text version, you'll need to encrypt it via your Staticman instance `https://yourstaticmaninstance/v3/encrypt/PLAINSECRET`. + ## Custom Comments Widget We don't intend to support all comments widgets, but don't worry, you can customize your own comments widget. diff --git a/exampleSite/content/docs/widgets/comments/index.zh-hans.md b/exampleSite/content/docs/widgets/comments/index.zh-hans.md index 37f39ebbf..83e0a504f 100644 --- a/exampleSite/content/docs/widgets/comments/index.zh-hans.md +++ b/exampleSite/content/docs/widgets/comments/index.zh-hans.md @@ -12,7 +12,8 @@ categories = [ tags = [ "Disqus", "Utterances", - "Giscus" + "Giscus", + "Staticman" ] series = [ "文档" @@ -99,6 +100,160 @@ disqusShortname = "yourdiscussshortname" | `giscus.lang` | String | - | Specify language, default to site language. | `giscus.lazyLoading` | Boolean | `true` | Enable lazy loading. +## Staticman + +[Staticman](https://staticman.net/) is also supported out of box, but it may be too complex to set it up. + +### Advantages + +- The comments files are stored inside your repository, that is, the comments are rendered during the site build. It maybe useful for SEO, since it doesn't rely JS to load data dynamically, it's truly static. +- Handling spam: approve or reject comment via Git provider pull request, Google reCaptcha. +- Native theme style. + +### Disadvantages + +- The user information are untrusted, such as email. +- User unable to delete their comments directly, it's able to do that via Pull Request. + +### Preparations + +#### Staticman instance + +Firstly, we should set up a Staticman instance for handling comments requests. + +##### Self-Hosted Staticman instance + +Please see https://staticman.net/docs/getting-started.html#step-2-deploy-staticman for details. + +##### Public Staticman instance + +I set up a public staticman instance for testing, it should works with GitHub repositories only. + +> You'll need to install the [GitHub App](https://github.com/apps/hbs-staticman) for your repo, so that the instance has access to write comments files to your repo. + +- Endpoint: https://hbs-staticman.herokuapp.com + +#### Staticman Configuration + +Secondary, we need to create the `staticman.yml` under your site/repository root, so that Staticman instance can read the configuration and process comments requests. + +{{% alert "warning" %}} +The `filename`, `path` are fixed, please **DO NOT** modify those parameters. +{{% /alert %}} + +{{% alert "warning" %}} +The `allowedFields` MUST include `name`, `email`, `message`, `reply_to`, `root_id` fields. +{{% /alert %}} + +```yaml +# Name of the property. You can have multiple properties with completely +# different config blocks for different sections of your site. +# For example, you can have one property to handle comment submission and +# another one to handle posts. +comments: + # (*) REQUIRED + # + # Names of the fields the form is allowed to submit. If a field that is + # not here is part of the request, an error will be thrown. + allowedFields: ["name", "email", "url", "message", "reply_to", "root_id"] + + # (*) REQUIRED + # + # Name of the branch being used. Must match the one sent in the URL of the + # request. + branch: "main" + + # Text to use as the commit message or pull request title. Accepts placeholders. + commitMessage: "Add Staticman comment" + + # (*) REQUIRED + # + # Destination path (filename) for the data files. Accepts placeholders. + filename: "{@id}" # DO NOT MODIFY + + # The format of the generated data files. Accepted values are "json", "yaml" + # or "frontmatter" + format: "yaml" + + # List of fields to be populated automatically by Staticman and included in + # the data file. Keys are the name of the field. The value can be an object + # with a `type` property, which configures the generated field, or any value + # to be used directly (e.g. a string, number or array) + generatedFields: + date: + type: date + options: + format: "timestamp-seconds" + + # Whether entries need to be appproved before they are published to the main + # branch. If set to `true`, a pull request will be created for your approval. + # Otherwise, entries will be published to the main branch automatically. + moderation: false + + # Name of the site. Used in notification emails. + name: "hbs.razonyang.com" + + # Notification settings. When enabled, users can choose to receive notifications + # via email when someone adds a reply or a new comment. This requires an account + # with Mailgun, which you can get for free at http://mailgun.com. + #notifications: + # Enable notifications + #enabled: true + + # (!) ENCRYPTED + # + # Mailgun API key + #apiKey: "1q2w3e4r" + + # (!) ENCRYPTED + # + # Mailgun domain (encrypted) + #domain: "4r3e2w1q" + + # (*) REQUIRED + # + # Destination path (directory) for the data files. Accepts placeholders. + path: "data/{options.slug}" # DO NOT MODIFY + + # Names of required fields. If any of these isn't in the request or is empty, + # an error will be thrown. + requiredFields: ["name", "message"] + + # List of transformations to apply to any of the fields supplied. Keys are + # the name of the field and values are possible transformation types. + transforms: + email: md5 # DO NOT MODIFY, REQUIRED BY AVATAR. + + reCaptcha: + enabled: false + siteKey: "" + secret: +``` + +#### Site Parameters + +Finally, tweak the following `staticman` parameters in `params.toml`. + +| Name | Type | Default | Description | +|:---|:---|:--:|:--- +| `staticman` | Object | | +| `staticman.endpoint` | String | - | THe Staticman instance endpoint. Required. +| `staticman.repo` | String | - | Repository that with form `user/repo`. Required. +| `staticman.service` | String | `github` | +| `staticman.branch` | String | `master` | Repository branch. +| `staticman.property` | String | `comments` | +| `staticman.sorting` | String | `asc` | Sorting comments, available options: `desc`. +| `staticman.reCaptchaKey` | String | `-` | The reCaptcha site key. +| `staticman.reCaptchaSecret` | String | `-` | The reCaptcha encrypted secret. You'll need to encrypt plain secret via https://yourstaticmaninstance/v3/encrypt/plainsecret. +| `staticman.extraFields` | Array | `-` | Extra fields. Available fiedls: `url`. +| `staticman.requiredFields` | Array | `-` | Extra required fields. Available fields: `email` and the extra fields. `name` and `message` are always required. +| `staticman.paginate` | Integer | `10` | The number of comments per page. +| `staticman.moderation` | Boolean | `true` | Same as Staticman's moderation. + +#### reCaptcha + +The reCaptcha secret is not the plain text version, you'll need to encrypt it via your Staticman instance `https://yourstaticmaninstance/v3/encrypt/PLAINSECRET`. + ## 自定义评论小部件 我们不打算支持所有的评论小部件,但别担心,你完全可以自定义评论小部件。 diff --git a/exampleSite/content/docs/widgets/comments/index.zh-hant.md b/exampleSite/content/docs/widgets/comments/index.zh-hant.md index c11abe0bc..ef93b7431 100644 --- a/exampleSite/content/docs/widgets/comments/index.zh-hant.md +++ b/exampleSite/content/docs/widgets/comments/index.zh-hant.md @@ -12,7 +12,8 @@ categories = [ tags = [ "Disqus", "Utterances", - "Giscus" + "Giscus", + "Staticman" ] series = [ "文檔" @@ -99,6 +100,160 @@ disqusShortname = "yourdiscussshortname" | `giscus.lang` | String | - | Specify language, default to site language. | `giscus.lazyLoading` | Boolean | `true` | Enable lazy loading. +## Staticman + +[Staticman](https://staticman.net/) is also supported out of box, but it may be too complex to set it up. + +### Advantages + +- The comments files are stored inside your repository, that is, the comments are rendered during the site build. It maybe useful for SEO, since it doesn't rely JS to load data dynamically, it's truly static. +- Handling spam: approve or reject comment via Git provider pull request, Google reCaptcha. +- Native theme style. + +### Disadvantages + +- The user information are untrusted, such as email. +- User unable to delete their comments directly, it's able to do that via Pull Request. + +### Preparations + +#### Staticman instance + +Firstly, we should set up a Staticman instance for handling comments requests. + +##### Self-Hosted Staticman instance + +Please see https://staticman.net/docs/getting-started.html#step-2-deploy-staticman for details. + +##### Public Staticman instance + +I set up a public staticman instance for testing, it should works with GitHub repositories only. + +> You'll need to install the [GitHub App](https://github.com/apps/hbs-staticman) for your repo, so that the instance has access to write comments files to your repo. + +- Endpoint: https://hbs-staticman.herokuapp.com + +#### Staticman Configuration + +Secondary, we need to create the `staticman.yml` under your site/repository root, so that Staticman instance can read the configuration and process comments requests. + +{{% alert "warning" %}} +The `filename`, `path` are fixed, please **DO NOT** modify those parameters. +{{% /alert %}} + +{{% alert "warning" %}} +The `allowedFields` MUST include `name`, `email`, `message`, `reply_to`, `root_id` fields. +{{% /alert %}} + +```yaml +# Name of the property. You can have multiple properties with completely +# different config blocks for different sections of your site. +# For example, you can have one property to handle comment submission and +# another one to handle posts. +comments: + # (*) REQUIRED + # + # Names of the fields the form is allowed to submit. If a field that is + # not here is part of the request, an error will be thrown. + allowedFields: ["name", "email", "url", "message", "reply_to", "root_id"] + + # (*) REQUIRED + # + # Name of the branch being used. Must match the one sent in the URL of the + # request. + branch: "main" + + # Text to use as the commit message or pull request title. Accepts placeholders. + commitMessage: "Add Staticman comment" + + # (*) REQUIRED + # + # Destination path (filename) for the data files. Accepts placeholders. + filename: "{@id}" # DO NOT MODIFY + + # The format of the generated data files. Accepted values are "json", "yaml" + # or "frontmatter" + format: "yaml" + + # List of fields to be populated automatically by Staticman and included in + # the data file. Keys are the name of the field. The value can be an object + # with a `type` property, which configures the generated field, or any value + # to be used directly (e.g. a string, number or array) + generatedFields: + date: + type: date + options: + format: "timestamp-seconds" + + # Whether entries need to be appproved before they are published to the main + # branch. If set to `true`, a pull request will be created for your approval. + # Otherwise, entries will be published to the main branch automatically. + moderation: false + + # Name of the site. Used in notification emails. + name: "hbs.razonyang.com" + + # Notification settings. When enabled, users can choose to receive notifications + # via email when someone adds a reply or a new comment. This requires an account + # with Mailgun, which you can get for free at http://mailgun.com. + #notifications: + # Enable notifications + #enabled: true + + # (!) ENCRYPTED + # + # Mailgun API key + #apiKey: "1q2w3e4r" + + # (!) ENCRYPTED + # + # Mailgun domain (encrypted) + #domain: "4r3e2w1q" + + # (*) REQUIRED + # + # Destination path (directory) for the data files. Accepts placeholders. + path: "data/{options.slug}" # DO NOT MODIFY + + # Names of required fields. If any of these isn't in the request or is empty, + # an error will be thrown. + requiredFields: ["name", "message"] + + # List of transformations to apply to any of the fields supplied. Keys are + # the name of the field and values are possible transformation types. + transforms: + email: md5 # DO NOT MODIFY, REQUIRED BY AVATAR. + + reCaptcha: + enabled: false + siteKey: "" + secret: +``` + +#### Site Parameters + +Finally, tweak the following `staticman` parameters in `params.toml`. + +| Name | Type | Default | Description | +|:---|:---|:--:|:--- +| `staticman` | Object | | +| `staticman.endpoint` | String | - | THe Staticman instance endpoint. Required. +| `staticman.repo` | String | - | Repository that with form `user/repo`. Required. +| `staticman.service` | String | `github` | +| `staticman.branch` | String | `master` | Repository branch. +| `staticman.property` | String | `comments` | +| `staticman.sorting` | String | `asc` | Sorting comments, available options: `desc`. +| `staticman.reCaptchaKey` | String | `-` | The reCaptcha site key. +| `staticman.reCaptchaSecret` | String | `-` | The reCaptcha encrypted secret. You'll need to encrypt plain secret via https://yourstaticmaninstance/v3/encrypt/plainsecret. +| `staticman.extraFields` | Array | `-` | Extra fields. Available fiedls: `url`. +| `staticman.requiredFields` | Array | `-` | Extra required fields. Available fields: `email` and the extra fields. `name` and `message` are always required. +| `staticman.paginate` | Integer | `10` | The number of comments per page. +| `staticman.moderation` | Boolean | `true` | Same as Staticman's moderation. + +#### reCaptcha + +The reCaptcha secret is not the plain text version, you'll need to encrypt it via your Staticman instance `https://yourstaticmaninstance/v3/encrypt/PLAINSECRET`. + ## 自定義評論小部件 我們不打算支持所有的評論小部件,但別擔心,你完全可以自定義評論小部件。 diff --git a/i18n/en.toml b/i18n/en.toml index 142aaa791..0de86ba39 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -79,6 +79,27 @@ other = "Yellow" [comments] other = "Comments" +[comments_email] +other = "Email" + +[comments_message] +other = "Message" + +[comments_name] +other = "Name" + +[comments_reply] +other = "Reply" + +[comments_reply_to] +other = "Reply to" + +[comments_submit] +other = "Submit" + +[comments_url] +other = "URL" + [contact_name] other = "Name" diff --git a/i18n/zh-cn.toml b/i18n/zh-cn.toml index 43d1a0048..951f48036 100644 --- a/i18n/zh-cn.toml +++ b/i18n/zh-cn.toml @@ -79,6 +79,27 @@ other = "黄色" [comments] other = "评论" +[comments_email] +other = "邮箱" + +[comments_message] +other = "内容" + +[comments_name] +other = "名称" + +[comments_reply] +other = "回复" + +[comments_reply_to] +other = "回复" + +[comments_submit] +other = "提交" + +[comments_url] +other = "URL" + [contact_name] other = "名称" diff --git a/i18n/zh-hans.toml b/i18n/zh-hans.toml index 43d1a0048..951f48036 100644 --- a/i18n/zh-hans.toml +++ b/i18n/zh-hans.toml @@ -79,6 +79,27 @@ other = "黄色" [comments] other = "评论" +[comments_email] +other = "邮箱" + +[comments_message] +other = "内容" + +[comments_name] +other = "名称" + +[comments_reply] +other = "回复" + +[comments_reply_to] +other = "回复" + +[comments_submit] +other = "提交" + +[comments_url] +other = "URL" + [contact_name] other = "名称" diff --git a/i18n/zh-hant.toml b/i18n/zh-hant.toml index ded0ca27d..b013f02f8 100644 --- a/i18n/zh-hant.toml +++ b/i18n/zh-hant.toml @@ -79,6 +79,27 @@ other = "黃色" [comments] other = "評論" +[comments_email] +other = "郵箱" + +[comments_message] +other = "內容" + +[comments_name] +other = "名稱" + +[comments_reply] +other = "回复" + +[comments_reply_to] +other = "回复" + +[comments_submit] +other = "提交" + +[comments_url] +other = "URL" + [contact_name] other = "名稱" diff --git a/i18n/zh-hk.toml b/i18n/zh-hk.toml index 2ccd76c0c..eb1dc393d 100644 --- a/i18n/zh-hk.toml +++ b/i18n/zh-hk.toml @@ -79,6 +79,27 @@ other = "黃色" [comments] other = "評論" +[comments_email] +other = "郵箱" + +[comments_message] +other = "內容" + +[comments_name] +other = "名稱" + +[comments_reply] +other = "回复" + +[comments_reply_to] +other = "回复" + +[comments_submit] +other = "提交" + +[comments_url] +other = "URL" + [contact_name] other = "名稱" diff --git a/i18n/zh-tw.toml b/i18n/zh-tw.toml index ded0ca27d..b013f02f8 100644 --- a/i18n/zh-tw.toml +++ b/i18n/zh-tw.toml @@ -79,6 +79,27 @@ other = "黃色" [comments] other = "評論" +[comments_email] +other = "郵箱" + +[comments_message] +other = "內容" + +[comments_name] +other = "名稱" + +[comments_reply] +other = "回复" + +[comments_reply_to] +other = "回复" + +[comments_submit] +other = "提交" + +[comments_url] +other = "URL" + [contact_name] other = "名稱" diff --git a/layouts/partials/assets/main/js.html b/layouts/partials/assets/main/js.html index 512c486f4..fec3e186c 100644 --- a/layouts/partials/assets/main/js.html +++ b/layouts/partials/assets/main/js.html @@ -9,6 +9,9 @@ {{- if .Site.Params.giscus -}} {{- $scripts = $scripts | append (resources.Get "js/giscus/index.ts") -}} {{- end -}} +{{- if .Site.Params.staticman -}} + {{- $scripts = $scripts | append (resources.Get "js/staticman/index.ts") -}} +{{- end -}} {{- $script := $scripts | resources.Concat "main/js/bundle.ts" | js.Build $options | fingerprint -}} {{- range .Site.Params.customJS -}} diff --git a/layouts/partials/post/comments.html b/layouts/partials/post/comments.html index befa508d9..1382e4f5c 100644 --- a/layouts/partials/post/comments.html +++ b/layouts/partials/post/comments.html @@ -8,6 +8,7 @@

{{ i18n "comments" }}

{{- partial "post/comments/disqus.html" . -}} {{- partial "post/comments/utterances" . -}} {{- partial "post/comments/giscus" . -}} + {{- partial "post/comments/staticman" . -}} {{- partial "post/comments/custom" . -}} diff --git a/layouts/partials/post/comments/staticman.html b/layouts/partials/post/comments/staticman.html new file mode 100644 index 000000000..df8da02b7 --- /dev/null +++ b/layouts/partials/post/comments/staticman.html @@ -0,0 +1,73 @@ +{{- if isset $.Site.Params "staticman" }} + {{- template "walk-comments" . }} + + {{/* generate comments pages */}} + {{- $paginate := default 10 $.Site.Params.staticman.paginate }} + {{- $sorting := default "asc" $.Site.Params.staticman.sorting }} + {{- $comments := .Scratch.Get "comments" }} + + {{- partial "staticman/recaptcha" . }} + {{- partial "staticman/reply-modal" . }} + {{- partial "staticman/form" . }} + + {{- if $comments }} + {{- $tmpl := resources.Get "templates/staticman/comments.html" }} + {{- $commentsPages := div (add (len $comments) (sub $paginate 1)) $paginate }} + {{- $page := . }} + {{- $homepage := .GetPage "/" }} + {{- range seq $commentsPages }} + {{- $commentsPageURL := replace (printf "%s/comments/page/%d.html" $.Permalink .) ($homepage.Permalink) "" }} + {{- $commentsPageURL = printf "%s/%s" $.Site.LanguagePrefix $commentsPageURL }} + {{- $page.Scratch.Set "commentsPage" . }} + {{- $commentsPage := $tmpl | resources.ExecuteAsTemplate $commentsPageURL $page }} + {{- $commentsPage.Permalink }} + {{- end }} + {{- partial "staticman/list" (dict + "Comments" $comments + "AllComments" (.Scratch.Get "allComments") + "Page" 1 + "Sorting" $sorting + "DateFormat" $.Site.Params.dateFormat + "PageSize" $paginate + ) }} + {{- partial "staticman/pagination" (dict + "TotalCount" (len $comments) + "Page" 1 + "PageSize" $paginate + "PageLink" $.Permalink + ) }} + {{- end }} +{{- end }} + +{{/* walk through the comments directory to collect comments and their children of current page */}} +{{- define "walk-comments" }} + {{- $scratch := .Scratch }} + {{- $slug := .File.UniqueID }} + {{- with index $.Site.Data .Lang }} + {{- with index .comments $slug }} + {{- $allComments := . }} + {{- $comments := dict }} + {{- $scratch.Set "allComments" $allComments -}} + {{- range sort $allComments "date" "asc" }} + {{- if .root_id }} + {{/* collect child comments */}} + {{- $child := . }} + {{/* ensure parent comment exists */}} + {{- if isset $allComments .root_id}} + {{- $parent := index $comments $child.root_id }} + {{- $children := $parent.Children | append $child }} + {{- $comments = merge $comments (dict $child.root_id (dict + "Children" $children + )) }} + {{- end }} + {{- else }} + {{- $comments = merge $comments (dict ._id (dict + "Comment" . + "Children" slice + )) }} + {{- end }} + {{- end }} + {{- $scratch.Set "comments" $comments }} + {{- end }} + {{- end }} +{{- end }} diff --git a/layouts/partials/staticman/comments.html b/layouts/partials/staticman/comments.html new file mode 100644 index 000000000..5664d530d --- /dev/null +++ b/layouts/partials/staticman/comments.html @@ -0,0 +1,36 @@ +
+ {{- partial "staticman/recaptcha" . }} + {{- partial "staticman/reply-modal" . }} + {{- $currentPage := .Scratch.Get "commentsPage" }} +
+
+

+ {{ i18n "comments" }} - {{ i18n "pagination_title" (dict "PageNumber" $currentPage) }} - {{ .Title }} +

+ {{- partial "staticman/form" . }} + {{- $paginate := default 10 $.Site.Params.staticman.paginate }} + {{- $sorting := default "asc" $.Site.Params.staticman.sorting }} + {{- $dateFormat := $.Site.Params.dateFormat }} + {{- $currentPage := .Scratch.Get "commentsPage" }} + {{- $allComments := .Scratch.Get "allComments" }} + {{- with .Scratch.Get "comments" }} + {{- $comments := . }} + {{- partial "staticman/list" (dict + "Comments" . + "AllComments" $allComments + "Page" $currentPage + "Sorting" $sorting + "DateFormat" $dateFormat + "PageSize" $paginate + ) }} + {{- partial "staticman/pagination" (dict + "TotalCount" (len .) + "Page" $currentPage + "PageSize" $paginate + "PageLink" $.Permalink + ) }} + {{- end }} +
+
+
+{{- partial "sidebar" . -}} diff --git a/layouts/partials/staticman/form-body.html b/layouts/partials/staticman/form-body.html new file mode 100644 index 000000000..f87b98041 --- /dev/null +++ b/layouts/partials/staticman/form-body.html @@ -0,0 +1,42 @@ +{{- $slug := printf "%s/comments/%s" .Lang .File.UniqueID }} +{{- $sorting := default "asc" $.Site.Params.staticman.sorting }} +{{- $reCaptchaKey := default "" $.Site.Params.staticman.reCaptchaKey }} +{{- $requiredFields := default slice $.Site.Params.staticman.requiredFields }} +{{- $extraFields := default slice $.Site.Params.staticman.extraFields }} + + + +
+
+ + +
+
+ + +
+ {{- if in $extraFields "url" }} +
+ + +
+ {{- end }} +
+ + +
+
+
+ {{- if $reCaptchaKey }} + +
+ {{- end }} + +
diff --git a/layouts/partials/staticman/form.html b/layouts/partials/staticman/form.html new file mode 100644 index 000000000..0dd4bab6e --- /dev/null +++ b/layouts/partials/staticman/form.html @@ -0,0 +1,3 @@ +
+ {{- partial "staticman/form-body" . }} +
diff --git a/layouts/partials/staticman/list-item.html b/layouts/partials/staticman/list-item.html new file mode 100644 index 000000000..dab61aa61 --- /dev/null +++ b/layouts/partials/staticman/list-item.html @@ -0,0 +1,44 @@ +{{- $dateFormat := .DateFormat }} +{{- $allComments := .AllComments }} +
+
+ + {{- if .Comment.url }} + {{ .Comment.name }} + {{- else }} + {{ .Comment.name }} + {{- end }} + {{ .Comment.date | int | time | time.Format $dateFormat }} +
+
+ {{- if .Comment.reply_to }} + {{- with index $allComments .Comment.reply_to }} + @{{ .name }} + {{- end }} + {{- end }} + {{- $message := replaceRE "(?i)]*>[^<]*" "~~invalid message~~" .Comment.message }} + {{- $message | markdownify }} +
+ {{- $rootId := default .Comment._id $.RootId }} +
+ +
+ {{- with .Children }} +
+ {{- range . }} + {{- partial "staticman/list-item" (dict + "AllComments" $allComments + "Comment" . + "Children" slice + "DateFormat" $dateFormat + "RootId" $rootId + ) }} + {{- end }} +
+ {{- end }} +
diff --git a/layouts/partials/staticman/list.html b/layouts/partials/staticman/list.html new file mode 100644 index 000000000..1bc65e093 --- /dev/null +++ b/layouts/partials/staticman/list.html @@ -0,0 +1,17 @@ + +{{- $max := mul .Page .PageSize }} +{{- $min := sub $max .PageSize }} +{{- $counter := 0 }} +
+{{- range sort .Comments ".Comment.date" .Sorting }} + {{- if and (ge $counter $min) (lt $counter $max) }} + {{- partial "staticman/list-item" (dict + "AllComments" $.AllComments + "Comment" .Comment + "Children" .Children + "DateFormat" $.DateFormat + ) }} + {{- end }} + {{- $counter = add $counter 1 }} +{{- end }} +
diff --git a/layouts/partials/staticman/pagination.html b/layouts/partials/staticman/pagination.html new file mode 100644 index 000000000..964b81bea --- /dev/null +++ b/layouts/partials/staticman/pagination.html @@ -0,0 +1,35 @@ +{{- $totalPages := div (add .TotalCount (sub .PageSize 1)) .PageSize }} +{{- $page := .Page }} +{{- $distance := 2 }} +{{- if gt $totalPages 1 -}} + +{{- end }} diff --git a/layouts/partials/staticman/recaptcha.html b/layouts/partials/staticman/recaptcha.html new file mode 100644 index 000000000..16965220f --- /dev/null +++ b/layouts/partials/staticman/recaptcha.html @@ -0,0 +1,27 @@ +{{- if $.Site.Params.staticman.reCaptchaKey }} + + +{{- end }} diff --git a/layouts/partials/staticman/reply-form.html b/layouts/partials/staticman/reply-form.html new file mode 100644 index 000000000..9420a8630 --- /dev/null +++ b/layouts/partials/staticman/reply-form.html @@ -0,0 +1,3 @@ +
+ {{- partial "staticman/form-body" . }} +
diff --git a/layouts/partials/staticman/reply-modal.html b/layouts/partials/staticman/reply-modal.html new file mode 100644 index 000000000..53b350947 --- /dev/null +++ b/layouts/partials/staticman/reply-modal.html @@ -0,0 +1,15 @@ + diff --git a/package.hugo.json b/package.hugo.json index 4bf409202..e066d145b 100644 --- a/package.hugo.json +++ b/package.hugo.json @@ -14,6 +14,7 @@ "@popperjs/core": "^2.11.2", "autoprefixer": "^10.4.2", "bootstrap": "github:twbs/bootstrap#new-masthead-darkmode", + "date-fns": "^2.29.3", "@docsearch/js": "^3.1.0", "fuse.js": "^6.5.3", "katex": "^0.15.2",