From b4b22aad94ed03e6a4a84c2bc2a17e50ff37a737 Mon Sep 17 00:00:00 2001
From: cromelex <96779452+cromelex@users.noreply.github.com>
Date: Tue, 13 May 2025 15:03:40 +0100
Subject: [PATCH 1/8] feat:ReplyByEmail button
adds the ability to add a ReplyByEmail button to your notes. Email address and pages to include/exclude can be added via layout file. Base64 encoding is used to *obfuscate* the email address from bots
---
quartz/components/ReplyByEmail.tsx | 142 +++++++++++++++++++++++++++++
quartz/components/index.ts | 2 +
2 files changed, 144 insertions(+)
create mode 100644 quartz/components/ReplyByEmail.tsx
diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx
new file mode 100644
index 000000000..b8f8450ef
--- /dev/null
+++ b/quartz/components/ReplyByEmail.tsx
@@ -0,0 +1,142 @@
+import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
+import { classNames } from "../util/lang"
+
+interface ReplyByEmailOptions {
+ username?: string
+ domain?: string
+ includeTitles?: string[]
+ excludeTitles?: string[]
+}
+
+// Default options will be used if not provided in the layout file
+const defaultOptions: ReplyByEmailOptions = {
+ username: "ZW1haWw=", // "email" in base64
+ domain: "ZXhhbXBsZS5jb20=", // "email.com" in base64
+ includeTitles: [],
+ excludeTitles: ["Home", "About me", "Contact me"]
+}
+
+const ReplyByEmail: QuartzComponent = ({
+ fileData,
+ displayClass,
+ username,
+ domain,
+ includeTitles,
+ excludeTitles
+}: QuartzComponentProps & ReplyByEmailOptions) => {
+ const title = fileData.frontmatter?.title
+
+ // Use provided values or defaults
+ const encodedPart1 = username || defaultOptions.username
+ const encodedPart2 = domain || defaultOptions.domain
+ const includeList = includeTitles || defaultOptions.includeTitles
+ const excludeList = excludeTitles || defaultOptions.excludeTitles
+
+ // Display logic:
+ // 1. If includeTitles is not empty, only show on those pages
+ // 2. If includeTitles is empty, show on all pages except those in excludeTitles
+ const shouldDisplay = title && (
+ (includeList.length > 0 && includeList.includes(title)) ||
+ (includeList.length === 0 && !excludeList.includes(title))
+ )
+
+ if (shouldDisplay) {
+ return (
+
+
+
+ )
+ } else {
+ return null
+ }
+}
+
+ReplyByEmail.css = `
+.center-wrapper {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 2rem;
+}
+
+.reply-by-email-button {
+ display: inline-block;
+ padding: 0.5rem 1rem;
+ background-color: var(--highlight);
+ color: var(--secondary);
+ border-radius: 5px;
+ transition: background-color 0.2s ease, transform 0.2s ease;
+ cursor: pointer;
+ border: none;
+ font-size: 1rem;
+ font-family: inherit;
+}
+
+.reply-by-email-button:hover {
+ transform: scale(1.05);
+}
+`
+
+// Script that works with SPA navigation
+ReplyByEmail.beforeDOMLoaded = `
+// Function to attach email button handlers
+function attachEmailHandlers() {
+ document.querySelectorAll('.reply-by-email-button').forEach(function(button) {
+ // Remove existing event listeners first to prevent duplicates
+ button.removeEventListener('click', handleEmailButtonClick);
+ // Add fresh event listener
+ button.addEventListener('click', handleEmailButtonClick);
+ });
+}
+
+// Handler function for the email button click
+function handleEmailButtonClick(e) {
+ e.preventDefault();
+
+ // Get data attributes
+ const username = atob(this.getAttribute('data-username'));
+ const domain = atob(this.getAttribute('data-domain'));
+ const title = this.getAttribute('data-title');
+
+ // Create email address and mailto link
+ const email = username + '@' + domain;
+ const mailtoLink = 'mailto:' + email + '?subject=' + title;
+
+ // Open email client
+ window.location.href = mailtoLink;
+}
+
+// Initial attachment when the page loads
+document.addEventListener('DOMContentLoaded', attachEmailHandlers);
+
+// Re-attach handlers after SPA navigation
+document.addEventListener('nav', function() {
+ // Small delay to ensure the new buttons are in the DOM
+ setTimeout(attachEmailHandlers, 10);
+});
+`
+
+export default ((opts?: ReplyByEmailOptions) => {
+ // Component constructor that accepts options
+ const component: QuartzComponent = (props) => {
+ return ReplyByEmail({
+ ...props,
+ username: opts?.username,
+ domain: opts?.domain,
+ includeTitles: opts?.includeTitles,
+ excludeTitles: opts?.excludeTitles
+ })
+ }
+
+ // Pass through the CSS and beforeDOMLoaded
+ component.css = ReplyByEmail.css
+ component.beforeDOMLoaded = ReplyByEmail.beforeDOMLoaded
+
+ return component
+}) satisfies QuartzComponentConstructor
diff --git a/quartz/components/index.ts b/quartz/components/index.ts
index cece8e614..b5aade6fb 100644
--- a/quartz/components/index.ts
+++ b/quartz/components/index.ts
@@ -23,6 +23,7 @@ import Breadcrumbs from "./Breadcrumbs"
import Comments from "./Comments"
import Flex from "./Flex"
import ConditionalRender from "./ConditionalRender"
+import ReplyByEmail from "./ReplyByEmail"
export {
ArticleTitle,
@@ -50,4 +51,5 @@ export {
Comments,
Flex,
ConditionalRender,
+ ReplyByEmail,
}
From e5049e89c6705b76a53fbde3a032bf89952f3722 Mon Sep 17 00:00:00 2001
From: cromelex <96779452+cromelex@users.noreply.github.com>
Date: Tue, 13 May 2025 15:29:46 +0100
Subject: [PATCH 2/8] feat:ReplyByEmail button
adds a 'buttonLabel' support to allow changing the ReplyByEmail button label from the layout file
---
quartz/components/ReplyByEmail.tsx | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx
index b8f8450ef..416d629db 100644
--- a/quartz/components/ReplyByEmail.tsx
+++ b/quartz/components/ReplyByEmail.tsx
@@ -6,6 +6,7 @@ interface ReplyByEmailOptions {
domain?: string
includeTitles?: string[]
excludeTitles?: string[]
+ buttonLabel?: string
}
// Default options will be used if not provided in the layout file
@@ -13,7 +14,8 @@ const defaultOptions: ReplyByEmailOptions = {
username: "ZW1haWw=", // "email" in base64
domain: "ZXhhbXBsZS5jb20=", // "email.com" in base64
includeTitles: [],
- excludeTitles: ["Home", "About me", "Contact me"]
+ excludeTitles: ["Home", "About me", "Contact me"],
+ buttonLabel: "Reply by email"
}
const ReplyByEmail: QuartzComponent = ({
@@ -22,7 +24,8 @@ const ReplyByEmail: QuartzComponent = ({
username,
domain,
includeTitles,
- excludeTitles
+ excludeTitles,
+ buttonLabel
}: QuartzComponentProps & ReplyByEmailOptions) => {
const title = fileData.frontmatter?.title
@@ -31,6 +34,7 @@ const ReplyByEmail: QuartzComponent = ({
const encodedPart2 = domain || defaultOptions.domain
const includeList = includeTitles || defaultOptions.includeTitles
const excludeList = excludeTitles || defaultOptions.excludeTitles
+ const label = buttonLabel || defaultOptions.buttonLabel
// Display logic:
// 1. If includeTitles is not empty, only show on those pages
@@ -49,7 +53,7 @@ const ReplyByEmail: QuartzComponent = ({
data-domain={encodedPart2}
data-title={encodeURIComponent(title)}
>
- Reply by email
+ {label}
)
@@ -130,7 +134,8 @@ export default ((opts?: ReplyByEmailOptions) => {
username: opts?.username,
domain: opts?.domain,
includeTitles: opts?.includeTitles,
- excludeTitles: opts?.excludeTitles
+ excludeTitles: opts?.excludeTitles,
+ buttonLabel: opts?.buttonLabel
})
}
From d94a68edb24d859e97c96dcc649bbb33775d2cac Mon Sep 17 00:00:00 2001
From: cromelex <96779452+cromelex@users.noreply.github.com>
Date: Tue, 13 May 2025 15:49:02 +0100
Subject: [PATCH 3/8] feat:ReplyByEmail button
fix formatting
---
quartz/components/ReplyByEmail.tsx | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx
index 416d629db..43a9807c6 100644
--- a/quartz/components/ReplyByEmail.tsx
+++ b/quartz/components/ReplyByEmail.tsx
@@ -47,14 +47,14 @@ const ReplyByEmail: QuartzComponent = ({
if (shouldDisplay) {
return (
-
+
)
} else {
From 64a101c0d9ca4df3c481e2c4a004178c7a01d007 Mon Sep 17 00:00:00 2001
From: cromelex <96779452+cromelex@users.noreply.github.com>
Date: Tue, 13 May 2025 16:40:14 +0100
Subject: [PATCH 4/8] feat:ReplyByEmail button
Add documentation for the component
---
docs/features/replybyemail button.md | 67 ++++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
create mode 100644 docs/features/replybyemail button.md
diff --git a/docs/features/replybyemail button.md b/docs/features/replybyemail button.md
new file mode 100644
index 000000000..ead09cf67
--- /dev/null
+++ b/docs/features/replybyemail button.md
@@ -0,0 +1,67 @@
+---
+title: Reply by Email Button
+tags:
+ - component
+---
+
+The Reply By Email Button is a feature that allows users to display a button under their notes, allowing visitors to send them an email to an obfuscated email address.
+- The email address is base64 encoded to provide some basic protection from bots.
+- The subject line of the email will be taken from the page Title where the button is clicked.
+- You can specify on what notes the button should be displayed or excluded.
+- The label on the button can also be customised.
+
+## Configuration
+
+The Reply By Email Button is disabled by default. To enable it, you can add the component to your layout configuration in `quartz.layout.ts`.
+
+Minimal configuration:
+```ts
+Component.ReplyByEmail({
+ username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com
+ domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com
+}),
+```
+You can specify under which notes to display the button by adding the following:
+```ts
+includeTitles: ["Welcome to Quartz 4", "Reply by Email Button"],
+```
+Alternatively, you can include the button by default by ommiting the `includeTitles` line, and specify notes under which the button should not be displayed:
+```ts
+excludeTitles: ["Welcome to Quartz 4", "Home"],
+```
+You can also override the default `buttonLabel` text:
+```ts
+buttonLabel: "Reply by email"
+```
+
+
+## Usage
+
+The natural placement for the Reply By Email Button is within `afterBody` in the Content Pages, so that it is displayed right under the note:
+
+```ts
+ afterBody: [
+ Component.ReplyByEmail({
+ username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com
+ domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com
+ // includeTitles: ["Welcome to Quartz 4"], // You can specify which page titles to include or comment out the line to include on all pages
+ excludeTitles: ["Welcome to Quartz 4", "Home"], // You can specify which page titles to exclude when includeTitles is empty
+ buttonLabel: "Submit feedback by email" // You can override the default button text "Reply by email"
+ }),
+ ],
+```
+
+## Customization
+
+You can customize the appearance of the ReplyByEmail button through CSS variables and styles. The component uses the following class:
+
+- `.reply-by-email-button`
+
+
+Example customization in your custom CSS:
+
+```scss
+.reply-by-email-button {
+ color: var(--tertiary);
+}
+```
From e8b3397f8cc5fb19ebbd9ed1012f3c236d760b03 Mon Sep 17 00:00:00 2001
From: cromelex <96779452+cromelex@users.noreply.github.com>
Date: Tue, 13 May 2025 16:55:29 +0100
Subject: [PATCH 5/8] feat:ReplyByEmail button
fix defaults
---
quartz/components/ReplyByEmail.tsx | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx
index 43a9807c6..91054faec 100644
--- a/quartz/components/ReplyByEmail.tsx
+++ b/quartz/components/ReplyByEmail.tsx
@@ -11,10 +11,10 @@ interface ReplyByEmailOptions {
// Default options will be used if not provided in the layout file
const defaultOptions: ReplyByEmailOptions = {
- username: "ZW1haWw=", // "email" in base64
- domain: "ZXhhbXBsZS5jb20=", // "email.com" in base64
+ username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com
+ domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com
includeTitles: [],
- excludeTitles: ["Home", "About me", "Contact me"],
+ excludeTitles: [],
buttonLabel: "Reply by email"
}
From 93aed112af988b64e4778eb583b246a403b1f27a Mon Sep 17 00:00:00 2001
From: cromelex <96779452+cromelex@users.noreply.github.com>
Date: Thu, 15 May 2025 23:50:35 +0100
Subject: [PATCH 6/8] feat:ReplyByEmail button
simplified and updated component
---
quartz/components/ReplyByEmail.tsx | 86 +++++++++---------------------
1 file changed, 24 insertions(+), 62 deletions(-)
diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx
index 91054faec..37a6baa6c 100644
--- a/quartz/components/ReplyByEmail.tsx
+++ b/quartz/components/ReplyByEmail.tsx
@@ -2,17 +2,13 @@ import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } fro
import { classNames } from "../util/lang"
interface ReplyByEmailOptions {
- username?: string
- domain?: string
+ email: string
includeTitles?: string[]
excludeTitles?: string[]
buttonLabel?: string
}
-// Default options will be used if not provided in the layout file
-const defaultOptions: ReplyByEmailOptions = {
- username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com
- domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com
+const defaultOptions: Partial = {
includeTitles: [],
excludeTitles: [],
buttonLabel: "Reply by email"
@@ -21,17 +17,15 @@ const defaultOptions: ReplyByEmailOptions = {
const ReplyByEmail: QuartzComponent = ({
fileData,
displayClass,
- username,
- domain,
+ email,
includeTitles,
excludeTitles,
buttonLabel
}: QuartzComponentProps & ReplyByEmailOptions) => {
const title = fileData.frontmatter?.title
- // Use provided values or defaults
- const encodedPart1 = username || defaultOptions.username
- const encodedPart2 = domain || defaultOptions.domain
+ const encodedEmail = btoa(email)
+
const includeList = includeTitles || defaultOptions.includeTitles
const excludeList = excludeTitles || defaultOptions.excludeTitles
const label = buttonLabel || defaultOptions.buttonLabel
@@ -47,15 +41,22 @@ const ReplyByEmail: QuartzComponent = ({
if (shouldDisplay) {
return (
-
-
+
)
} else {
return null
@@ -87,61 +88,22 @@ ReplyByEmail.css = `
}
`
-// Script that works with SPA navigation
-ReplyByEmail.beforeDOMLoaded = `
-// Function to attach email button handlers
-function attachEmailHandlers() {
- document.querySelectorAll('.reply-by-email-button').forEach(function(button) {
- // Remove existing event listeners first to prevent duplicates
- button.removeEventListener('click', handleEmailButtonClick);
- // Add fresh event listener
- button.addEventListener('click', handleEmailButtonClick);
- });
-}
-
-// Handler function for the email button click
-function handleEmailButtonClick(e) {
- e.preventDefault();
-
- // Get data attributes
- const username = atob(this.getAttribute('data-username'));
- const domain = atob(this.getAttribute('data-domain'));
- const title = this.getAttribute('data-title');
-
- // Create email address and mailto link
- const email = username + '@' + domain;
- const mailtoLink = 'mailto:' + email + '?subject=' + title;
-
- // Open email client
- window.location.href = mailtoLink;
-}
-
-// Initial attachment when the page loads
-document.addEventListener('DOMContentLoaded', attachEmailHandlers);
-
-// Re-attach handlers after SPA navigation
-document.addEventListener('nav', function() {
- // Small delay to ensure the new buttons are in the DOM
- setTimeout(attachEmailHandlers, 10);
-});
-`
-
export default ((opts?: ReplyByEmailOptions) => {
- // Component constructor that accepts options
+ if (!opts?.email) {
+ throw new Error("ReplyByEmail component requires an email parameter")
+ }
+
const component: QuartzComponent = (props) => {
return ReplyByEmail({
...props,
- username: opts?.username,
- domain: opts?.domain,
+ email: opts.email,
includeTitles: opts?.includeTitles,
excludeTitles: opts?.excludeTitles,
buttonLabel: opts?.buttonLabel
})
}
- // Pass through the CSS and beforeDOMLoaded
component.css = ReplyByEmail.css
- component.beforeDOMLoaded = ReplyByEmail.beforeDOMLoaded
return component
}) satisfies QuartzComponentConstructor
From 5a74a17d9b1f8f1c885501dac50dd7888d16be26 Mon Sep 17 00:00:00 2001
From: cromelex <96779452+cromelex@users.noreply.github.com>
Date: Thu, 15 May 2025 23:51:22 +0100
Subject: [PATCH 7/8] feat:ReplyByEmail button
Updated documentation for the component
---
docs/features/replybyemail button.md | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/docs/features/replybyemail button.md b/docs/features/replybyemail button.md
index ead09cf67..77092a32f 100644
--- a/docs/features/replybyemail button.md
+++ b/docs/features/replybyemail button.md
@@ -4,9 +4,9 @@ tags:
- component
---
-The Reply By Email Button is a feature that allows users to display a button under their notes, allowing visitors to send them an email to an obfuscated email address.
+The Reply By Email Button is a feature that allows users to display a button under their notes, allowing visitors to comment or submited feedback for your notes via email.
- The email address is base64 encoded to provide some basic protection from bots.
-- The subject line of the email will be taken from the page Title where the button is clicked.
+- The subject line of the email will be taken from the page title where the button is clicked.
- You can specify on what notes the button should be displayed or excluded.
- The label on the button can also be customised.
@@ -17,13 +17,12 @@ The Reply By Email Button is disabled by default. To enable it, you can add the
Minimal configuration:
```ts
Component.ReplyByEmail({
- username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com
- domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com
+ email: "contact@example.com"
}),
```
You can specify under which notes to display the button by adding the following:
```ts
-includeTitles: ["Welcome to Quartz 4", "Reply by Email Button"],
+includeTitles: ["Welcome to Quartz 4", "Reply by Email Button", "Contact"],
```
Alternatively, you can include the button by default by ommiting the `includeTitles` line, and specify notes under which the button should not be displayed:
```ts
@@ -42,8 +41,7 @@ The natural placement for the Reply By Email Button is within `afterBody` in the
```ts
afterBody: [
Component.ReplyByEmail({
- username: "Y29udGFjdA==", // "contact" encoded in base64, as in contact@example.com
- domain: "ZXhhbXBsZS5jb20=", // "example.com" encoded in base64, as in contact@example.com
+ email: "contact@example.com", // The email address to be linked to
// includeTitles: ["Welcome to Quartz 4"], // You can specify which page titles to include or comment out the line to include on all pages
excludeTitles: ["Welcome to Quartz 4", "Home"], // You can specify which page titles to exclude when includeTitles is empty
buttonLabel: "Submit feedback by email" // You can override the default button text "Reply by email"
From 593f8b0aed68034fc3e66ef71e01f1fe280f6a3f Mon Sep 17 00:00:00 2001
From: cromelex <96779452+cromelex@users.noreply.github.com>
Date: Thu, 15 May 2025 23:53:25 +0100
Subject: [PATCH 8/8] feat:ReplyByEmail button
fix formatting
---
quartz/components/ReplyByEmail.tsx | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/quartz/components/ReplyByEmail.tsx b/quartz/components/ReplyByEmail.tsx
index 37a6baa6c..7c1b751dd 100644
--- a/quartz/components/ReplyByEmail.tsx
+++ b/quartz/components/ReplyByEmail.tsx
@@ -41,11 +41,11 @@ const ReplyByEmail: QuartzComponent = ({
if (shouldDisplay) {
return (