mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-01-09 14:18:02 -05:00
feat: normalize image paths for main image (#589)
This commit is contained in:
@@ -8,7 +8,7 @@ tags:
|
|||||||
- privacy
|
- privacy
|
||||||
- user experience
|
- user experience
|
||||||
- privacy experience
|
- privacy experience
|
||||||
cover: "/privacy-experience-report.webp" # only if post uses a cover image
|
image: "/articles/privacy-experience-report/privacy-experience-report.webp"
|
||||||
---
|
---
|
||||||
|
|
||||||
## Purpose of the Research
|
## Purpose of the Research
|
||||||
@@ -34,7 +34,7 @@ Through qualitative interviews, we identified recurring themes across all tools
|
|||||||
|
|
||||||
## **Overall Conclusion**
|
## **Overall Conclusion**
|
||||||
|
|
||||||
Users don’t reject privacy, they reject *invisible, unverified, or cognitively heavy privacy*. To expand adoption, privacy tools must evolve from “power-user cryptography” to *trustable, testable, and human-centered infrastructure.*
|
Users don’t reject privacy, they reject _invisible, unverified, or cognitively heavy privacy_. To expand adoption, privacy tools must evolve from “power-user cryptography” to _trustable, testable, and human-centered infrastructure._
|
||||||
|
|
||||||
## **Acknowledgements**
|
## **Acknowledgements**
|
||||||
|
|
||||||
@@ -48,33 +48,31 @@ These projects represent the forefront of privacy innovation in the Ethereum eco
|
|||||||
|
|
||||||
We conducted **five one-on-one qualitative interviews** to explore how users interact with different privacy-focused on-chain tools and to understand their perceptions, challenges, and motivations around using them.
|
We conducted **five one-on-one qualitative interviews** to explore how users interact with different privacy-focused on-chain tools and to understand their perceptions, challenges, and motivations around using them.
|
||||||
|
|
||||||
Each participant was assigned **one privacy product**—either MEV Protection (Flashbots), Shielded Voting DAO, Privacy Pool, Fluidkey, or Railgun, along with a **specific usage task** (e.g., *create an account and deposit funds*). Participants were asked to **think aloud** as they completed the task, while the interviewer observed and probed to clarify their reasoning, expectations, and emotional responses.
|
Each participant was assigned **one privacy product**—either MEV Protection (Flashbots), Shielded Voting DAO, Privacy Pool, Fluidkey, or Railgun, along with a **specific usage task** (e.g., _create an account and deposit funds_). Participants were asked to **think aloud** as they completed the task, while the interviewer observed and probed to clarify their reasoning, expectations, and emotional responses.
|
||||||
|
|
||||||
Following the task, participants reflected on their **overall experience**, discussing what felt intuitive or confusing, what built or reduced trust, and whether they would consider using the tool again—and under what circumstances.
|
Following the task, participants reflected on their **overall experience**, discussing what felt intuitive or confusing, what built or reduced trust, and whether they would consider using the tool again—and under what circumstances.
|
||||||
|
|
||||||
## Thematic affinity mapping
|
## Thematic affinity mapping
|
||||||
|
|
||||||
1. *Grouping similar sentiments like “Confusing trust boundary” or “Fear of revealing info unknowingly”*
|
1. _Grouping similar sentiments like “Confusing trust boundary” or “Fear of revealing info unknowingly”_
|
||||||
2. *Color coding for which product each feedback came from — that way you can see patterns across categories*
|
2. _Color coding for which product each feedback came from — that way you can see patterns across categories_
|
||||||
|
|
||||||
### **Pattern 1: Confusion Around What’s Actually Private**
|
### **Pattern 1: Confusion Around What’s Actually Private**
|
||||||
|
|
||||||
*Behavior: Users frequently misunderstood what data or actions were protected versus exposed.*
|
_Behavior: Users frequently misunderstood what data or actions were protected versus exposed._
|
||||||
|
|
||||||
- Many assumed “shielded” meant *full anonymity*, only to discover that votes or transactions were private *temporarily* or *partially*.
|
- Many assumed “shielded” meant _full anonymity_, only to discover that votes or transactions were private _temporarily_ or _partially_.
|
||||||
- Participants were unsure when privacy applied. E.g., whether frontends, relayers, or RPCs could still leak information.
|
- Participants were unsure when privacy applied. E.g., whether frontends, relayers, or RPCs could still leak information.
|
||||||
|
|
||||||
**Quotes & Evidence:**
|
**Quotes & Evidence:**
|
||||||
|
|
||||||
> “I thought shielded would mean my vote would always be private… weird that I had to hover to see details.”
|
> “I thought shielded would mean my vote would always be private… weird that I had to hover to see details.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Snapshot UI
|
Snapshot UI
|
||||||
|
|
||||||
> “There are so many leaks if I’m using Alchemy… what is the point?”
|
> “There are so many leaks if I’m using Alchemy… what is the point?”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -82,11 +80,11 @@ Privacy Pool Github
|
|||||||
|
|
||||||
**Design implication:**
|
**Design implication:**
|
||||||
|
|
||||||
→ Tools need **explicit, contextual privacy indicators** (e.g., *“Your address is hidden until reveal phase”*) and **plain-language explanations** of privacy boundaries.
|
→ Tools need **explicit, contextual privacy indicators** (e.g., _“Your address is hidden until reveal phase”_) and **plain-language explanations** of privacy boundaries.
|
||||||
|
|
||||||
### **Pattern 2: Lack of Trust Transparency**
|
### **Pattern 2: Lack of Trust Transparency**
|
||||||
|
|
||||||
*Behavior: Trust decisions were driven by brand reputation, not by verifiable or visible assurances.*
|
_Behavior: Trust decisions were driven by brand reputation, not by verifiable or visible assurances._
|
||||||
|
|
||||||
- Users “trusted” Flashbots or Railgun because they’d heard of them, not because the interface provided proof.
|
- Users “trusted” Flashbots or Railgun because they’d heard of them, not because the interface provided proof.
|
||||||
- Even technically advanced users questioned how much custody or data the service retained.
|
- Even technically advanced users questioned how much custody or data the service retained.
|
||||||
@@ -94,20 +92,16 @@ Privacy Pool Github
|
|||||||
**Quotes:**
|
**Quotes:**
|
||||||
|
|
||||||
> “I trusted Shutter because the personal risk is low and I’ve heard of them, not because the UI proved anything.”
|
> “I trusted Shutter because the personal risk is low and I’ve heard of them, not because the UI proved anything.”
|
||||||
>
|
|
||||||
|
|
||||||
> “I’ve heard of Railgun before, so I’d trust it a little bit more”
|
> “I’ve heard of Railgun before, so I’d trust it a little bit more”
|
||||||
>
|
|
||||||
|
|
||||||
> “If the last release was three months ago and not many stars, I don’t feel confident.”
|
> “If the last release was three months ago and not many stars, I don’t feel confident.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Railgun Github
|
Railgun Github
|
||||||
|
|
||||||
> “Only you and Fluidkey can see all your transactions… Fluidkey team? Operator? What does that mean?”
|
> “Only you and Fluidkey can see all your transactions… Fluidkey team? Operator? What does that mean?”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -119,7 +113,7 @@ Fluidkey UI
|
|||||||
|
|
||||||
### **Pattern 3: Overly Technical Setup and Cognitive Overload**
|
### **Pattern 3: Overly Technical Setup and Cognitive Overload**
|
||||||
|
|
||||||
*Behavior: Participants found setup flows fragmented, verbose, or opaque, especially when required to buy ENS, deploy tokens, or manage RPCs.*
|
_Behavior: Participants found setup flows fragmented, verbose, or opaque, especially when required to buy ENS, deploy tokens, or manage RPCs._
|
||||||
|
|
||||||
- Even power users noted “a ton of clicks and signatures” with little feedback on what each did.
|
- Even power users noted “a ton of clicks and signatures” with little feedback on what each did.
|
||||||
- Non-technical users struggled to understand why new wallets, seeds, or denominations were needed.
|
- Non-technical users struggled to understand why new wallets, seeds, or denominations were needed.
|
||||||
@@ -127,10 +121,8 @@ Fluidkey UI
|
|||||||
**Quotes:**
|
**Quotes:**
|
||||||
|
|
||||||
> “There were a ton of clicks and signatures, I didn’t even know what I was agreeing to.”
|
> “There were a ton of clicks and signatures, I didn’t even know what I was agreeing to.”
|
||||||
>
|
|
||||||
|
|
||||||
> “Why do I need to buy an ENS just to test?”
|
> “Why do I need to buy an ENS just to test?”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -141,7 +133,6 @@ Snapshot UI
|
|||||||
Snapshot UI
|
Snapshot UI
|
||||||
|
|
||||||
> “I would never trust online generated seed, that’s the basic of crypto security.”
|
> “I would never trust online generated seed, that’s the basic of crypto security.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -153,7 +144,7 @@ Privacy Pool UI
|
|||||||
|
|
||||||
### **Pattern 4: Usability Frictions: Defaults, Navigation, and Feedback**
|
### **Pattern 4: Usability Frictions: Defaults, Navigation, and Feedback**
|
||||||
|
|
||||||
*Behavior: Users struggled with hidden controls, unclear defaults, and missing confirmations.*
|
_Behavior: Users struggled with hidden controls, unclear defaults, and missing confirmations._
|
||||||
|
|
||||||
- Privacy options were buried (“tiny text in the Voting tab”).
|
- Privacy options were buried (“tiny text in the Voting tab”).
|
||||||
- Defaults often undermined privacy (“Any” = public).
|
- Defaults often undermined privacy (“Any” = public).
|
||||||
@@ -162,17 +153,14 @@ Privacy Pool UI
|
|||||||
**Quotes:**
|
**Quotes:**
|
||||||
|
|
||||||
> “Defaults matter, it should default to shielded.”
|
> “Defaults matter, it should default to shielded.”
|
||||||
>
|
|
||||||
|
|
||||||
> “Where are the privacy controls? It’s just this tiny text.”
|
> “Where are the privacy controls? It’s just this tiny text.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Snapshot/Shutter UI
|
Snapshot/Shutter UI
|
||||||
|
|
||||||
> “If it’s private by default, that’s perfect. I shouldn’t have to think about it.”
|
> “If it’s private by default, that’s perfect. I shouldn’t have to think about it.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -184,7 +172,7 @@ Flashbot UI
|
|||||||
|
|
||||||
### **Pattern 5: Verification Anxiety and Fear of Loss**
|
### **Pattern 5: Verification Anxiety and Fear of Loss**
|
||||||
|
|
||||||
*Behavior: Users feared doing irreversible or unverified actions (e.g., sending funds or proofs without visible confirmation).*
|
_Behavior: Users feared doing irreversible or unverified actions (e.g., sending funds or proofs without visible confirmation)._
|
||||||
|
|
||||||
- Several wanted test modes or dry runs before risking real funds.
|
- Several wanted test modes or dry runs before risking real funds.
|
||||||
- Even confident users double-checked contract addresses or waited to see funds reappear.
|
- Even confident users double-checked contract addresses or waited to see funds reappear.
|
||||||
@@ -192,14 +180,12 @@ Flashbot UI
|
|||||||
**Quotes:**
|
**Quotes:**
|
||||||
|
|
||||||
> “There’s no testing mode. I wouldn’t send 1 ETH through something untested.”
|
> “There’s no testing mode. I wouldn’t send 1 ETH through something untested.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Flashbot UI
|
Flashbot UI
|
||||||
|
|
||||||
> “I want to see the contract before confirming the transaction.”
|
> “I want to see the contract before confirming the transaction.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -210,7 +196,6 @@ Etherscan of Privacy Pool tx
|
|||||||
Privacy Pool contract on Etherscan
|
Privacy Pool contract on Etherscan
|
||||||
|
|
||||||
> “I wouldn’t download something random, even on this machine.”
|
> “I wouldn’t download something random, even on this machine.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -222,7 +207,7 @@ Railgun UI
|
|||||||
|
|
||||||
### **Pattern 6: Context-Specific Privacy Motivation**
|
### **Pattern 6: Context-Specific Privacy Motivation**
|
||||||
|
|
||||||
*Behavior: Motivation to use privacy tools varied by context.*
|
_Behavior: Motivation to use privacy tools varied by context._
|
||||||
|
|
||||||
- Some wanted privacy for governance (voting), others only for large transfers or identity separation.
|
- Some wanted privacy for governance (voting), others only for large transfers or identity separation.
|
||||||
- “Compliant privacy” was seen by technical users as “not real privacy.”
|
- “Compliant privacy” was seen by technical users as “not real privacy.”
|
||||||
@@ -230,10 +215,8 @@ Railgun UI
|
|||||||
**Quotes:**
|
**Quotes:**
|
||||||
|
|
||||||
> “Compliant privacy is like giving up, it’s not really privacy at all.”
|
> “Compliant privacy is like giving up, it’s not really privacy at all.”
|
||||||
>
|
|
||||||
|
|
||||||
> “For large fund transfers I’d plan ahead, so waiting isn’t a big issue.”
|
> “For large fund transfers I’d plan ahead, so waiting isn’t a big issue.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -245,7 +228,7 @@ Privacy Pool UI
|
|||||||
|
|
||||||
### **Pattern 7: Educational Gaps and Mental Model Mismatches**
|
### **Pattern 7: Educational Gaps and Mental Model Mismatches**
|
||||||
|
|
||||||
*Behavior: Even advanced users struggled to articulate how features like stealth addresses, shielded voting, or relayers work.*
|
_Behavior: Even advanced users struggled to articulate how features like stealth addresses, shielded voting, or relayers work._
|
||||||
|
|
||||||
- Ambiguous labels (“Power user,” “Shielded,” “ASP”) created anxiety or alienation.
|
- Ambiguous labels (“Power user,” “Shielded,” “ASP”) created anxiety or alienation.
|
||||||
- Users appreciated inline explanations and step-by-step guidance.
|
- Users appreciated inline explanations and step-by-step guidance.
|
||||||
@@ -253,14 +236,12 @@ Privacy Pool UI
|
|||||||
**Quotes:**
|
**Quotes:**
|
||||||
|
|
||||||
> “A normal user probably doesn’t know what stealth addresses are, even I’m not sure I could define it.”
|
> “A normal user probably doesn’t know what stealth addresses are, even I’m not sure I could define it.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Fluidkey UI
|
Fluidkey UI
|
||||||
|
|
||||||
> “‘Power user’ makes me feel like maybe I’m not technical enough.”
|
> “‘Power user’ makes me feel like maybe I’m not technical enough.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -272,7 +253,7 @@ Fluidkey UI
|
|||||||
|
|
||||||
### **Pattern 8: Desired Qualities in Privacy Tools**
|
### **Pattern 8: Desired Qualities in Privacy Tools**
|
||||||
|
|
||||||
*Behavior: Across all interviews, users consistently valued:*
|
_Behavior: Across all interviews, users consistently valued:_
|
||||||
|
|
||||||
- **Transparency:** showing what’s happening
|
- **Transparency:** showing what’s happening
|
||||||
- **Control:** ability to verify and customize
|
- **Control:** ability to verify and customize
|
||||||
@@ -282,32 +263,30 @@ Fluidkey UI
|
|||||||
**Quotes:**
|
**Quotes:**
|
||||||
|
|
||||||
> “Anything that makes me feel a little bit more safe is important, like links to audits, social proof.”
|
> “Anything that makes me feel a little bit more safe is important, like links to audits, social proof.”
|
||||||
>
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Fluidkey UI
|
Fluidkey UI
|
||||||
|
|
||||||
> “Older apps that have been around longer feel safer.”
|
> “Older apps that have been around longer feel safer.”
|
||||||
>
|
|
||||||
|
|
||||||
**Design implication:**
|
**Design implication:**
|
||||||
|
|
||||||
→ Frame privacy as *trustable infrastructure* — emphasizing stability, safety, and proof over abstraction.
|
→ Frame privacy as _trustable infrastructure_ — emphasizing stability, safety, and proof over abstraction.
|
||||||
|
|
||||||
## Map to p**ain points vs opportunities and provide design suggestions**
|
## Map to p**ain points vs opportunities and provide design suggestions**
|
||||||
|
|
||||||
*Summarize the themes we identified as pain points and opportunities*
|
_Summarize the themes we identified as pain points and opportunities_
|
||||||
|
|
||||||
| Theme | Core Pain Point | Design Opportunity |
|
| Theme | Core Pain Point | Design Opportunity |
|
||||||
| --- | --- | --- |
|
| ----------------------------- | ------------------------------- | ----------------------------------------- |
|
||||||
| 1. Clarity of privacy scope | Users can’t tell what’s private | Add visible privacy indicators |
|
| 1. Clarity of privacy scope | Users can’t tell what’s private | Add visible privacy indicators |
|
||||||
| 2. Trust verification | Users rely on brand, not proof | Include audits and on-chain verifiability |
|
| 2. Trust verification | Users rely on brand, not proof | Include audits and on-chain verifiability |
|
||||||
| 3. Technical friction | Setup is complex | Simplify and guide onboarding |
|
| 3. Technical friction | Setup is complex | Simplify and guide onboarding |
|
||||||
| 4. Default behaviors | Wrong defaults expose users | Privacy-by-default UI |
|
| 4. Default behaviors | Wrong defaults expose users | Privacy-by-default UI |
|
||||||
| 5. Fear of loss | Lack of testing or visibility | Provide test mode and confirmations |
|
| 5. Fear of loss | Lack of testing or visibility | Provide test mode and confirmations |
|
||||||
| 6. Varying privacy motivation | Context-dependent needs | Offer adaptive privacy modes |
|
| 6. Varying privacy motivation | Context-dependent needs | Offer adaptive privacy modes |
|
||||||
| 7. Education & communication | Jargon-heavy UX | Layered explanations, plain language |
|
| 7. Education & communication | Jargon-heavy UX | Layered explanations, plain language |
|
||||||
|
|
||||||
## **Call to Action: Shaping the Future of On-Chain Privacy**
|
## **Call to Action: Shaping the Future of On-Chain Privacy**
|
||||||
|
|
||||||
@@ -316,20 +295,20 @@ This research is an open invitation to the ecosystem. We hope designers, develop
|
|||||||
**Contribute to Future Work:**
|
**Contribute to Future Work:**
|
||||||
|
|
||||||
1. **Identify Technical Challenges**
|
1. **Identify Technical Challenges**
|
||||||
|
|
||||||
Many user pain points appear UX-related but are rooted in deep technical limitations. Building verifiable privacy, safe testing, and seamless defaults requires cryptographic innovation, infrastructure evolution, and better developer tooling.
|
Many user pain points appear UX-related but are rooted in deep technical limitations. Building verifiable privacy, safe testing, and seamless defaults requires cryptographic innovation, infrastructure evolution, and better developer tooling.
|
||||||
|
|
||||||
2. **Expand Quantitative Understanding**
|
2. **Expand Quantitative Understanding**
|
||||||
|
|
||||||
Complement this qualitative study with large-scale quantitative analysis (We’re actively collecting responses at Devconnect! Fill out [the survey here](https://pad.ethereum.org/form/#/2/form/view/IFZv0NuHEXd-eqIBh0o+C88F9V6+WVcBGKEb1d2LJcE/) for us to better understand your perspective on privacy tools). Measure and prioritize privacy needs, attitudes, and usage barriers across user segments. Like technical vs. non-technical, high vs. low privacy motivation, guiding where investment will have the most impact.
|
Complement this qualitative study with large-scale quantitative analysis (We’re actively collecting responses at Devconnect! Fill out [the survey here](https://pad.ethereum.org/form/#/2/form/view/IFZv0NuHEXd-eqIBh0o+C88F9V6+WVcBGKEb1d2LJcE/) for us to better understand your perspective on privacy tools). Measure and prioritize privacy needs, attitudes, and usage barriers across user segments. Like technical vs. non-technical, high vs. low privacy motivation, guiding where investment will have the most impact.
|
||||||
|
|
||||||
3. **Prototype and Share Solutions**
|
3. **Prototype and Share Solutions**
|
||||||
|
|
||||||
Pilot “privacy-by-default” interfaces, testnet-safe flows, and verifiable trust cues. Publish learnings openly to accelerate shared progress.
|
Pilot “privacy-by-default” interfaces, testnet-safe flows, and verifiable trust cues. Publish learnings openly to accelerate shared progress.
|
||||||
|
|
||||||
4. **Build an Open Privacy UX Community**
|
4. **Build an Open Privacy UX Community**
|
||||||
|
|
||||||
If you’re a designer, developer, or researcher passionate about privacy experience, contribute ideas, case studies, or experiments. Together, we can make privacy a *default expectation,* but not an afterthought.
|
If you’re a designer, developer, or researcher passionate about privacy experience, contribute ideas, case studies, or experiments. Together, we can make privacy a _default expectation,_ but not an afterthought.
|
||||||
|
|
||||||
5. **Broaden Role and Feature Coverage**
|
5. **Broaden Role and Feature Coverage**
|
||||||
This study focused on specific user roles and product features. For instance, DAO managers in governance tools or deposit flows in privacy wallets. Future research should explore the full ecosystem of participants and functionalities to provide a more holistic view of the Privacy Experience (PX) across contexts.
|
This study focused on specific user roles and product features. For instance, DAO managers in governance tools or deposit flows in privacy wallets. Future research should explore the full ecosystem of participants and functionalities to provide a more holistic view of the Privacy Experience (PX) across contexts.
|
||||||
|
|||||||
115
lib/content.ts
115
lib/content.ts
@@ -3,68 +3,61 @@ import matter from "gray-matter"
|
|||||||
import jsYaml from "js-yaml"
|
import jsYaml from "js-yaml"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
|
||||||
// Valid base paths for images in the public folder
|
|
||||||
const VALID_IMAGE_BASES = ["articles", "projects", "project", "project-banners"]
|
const VALID_IMAGE_BASES = ["articles", "projects", "project", "project-banners"]
|
||||||
|
|
||||||
/**
|
function normalizeImagePath(
|
||||||
* Normalizes image paths in markdown content to ensure they resolve correctly.
|
imagePath: string | undefined,
|
||||||
* Handles various user input formats and converts them to /{basePath}/[slug]/[filename]
|
defaultBasePath: string = "articles",
|
||||||
* @param content - The markdown content to process
|
slug?: string
|
||||||
* @param defaultBasePath - The default base path to use if none is detected (e.g., "articles" or "projects")
|
): string | undefined {
|
||||||
*/
|
if (!imagePath) return undefined
|
||||||
function normalizeContentImagePaths(
|
|
||||||
content: string,
|
|
||||||
defaultBasePath: string = "articles"
|
|
||||||
): string {
|
|
||||||
const normalizeImagePath = (imagePath: string): string => {
|
|
||||||
// Skip external URLs, data URIs, and anchor links
|
|
||||||
if (
|
|
||||||
imagePath.startsWith("http://") ||
|
|
||||||
imagePath.startsWith("https://") ||
|
|
||||||
imagePath.startsWith("data:") ||
|
|
||||||
imagePath.startsWith("#")
|
|
||||||
) {
|
|
||||||
return imagePath
|
|
||||||
}
|
|
||||||
|
|
||||||
let normalized = imagePath.trim()
|
if (
|
||||||
|
imagePath.startsWith("http://") ||
|
||||||
// Remove leading ./ or ../
|
imagePath.startsWith("https://") ||
|
||||||
normalized = normalized.replace(/^(?:\.\.?\/)+/, "")
|
imagePath.startsWith("data:") ||
|
||||||
|
imagePath.startsWith("#")
|
||||||
// Remove leading /
|
) {
|
||||||
normalized = normalized.replace(/^\/+/, "")
|
return imagePath
|
||||||
|
|
||||||
// Remove public/ prefix (with or without leading slash already handled)
|
|
||||||
normalized = normalized.replace(/^public\//, "")
|
|
||||||
|
|
||||||
// Check if path already starts with a valid base
|
|
||||||
const hasValidBase = VALID_IMAGE_BASES.some((base) =>
|
|
||||||
normalized.startsWith(`${base}/`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// If no valid base, prepend the default base path
|
|
||||||
if (!hasValidBase) {
|
|
||||||
normalized = `${defaultBasePath}/${normalized}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add leading /
|
|
||||||
return `/${normalized}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match markdown images:  - handles optional title
|
let normalized = imagePath.trim()
|
||||||
|
normalized = normalized.replace(/^(?:\.\.?\/)+/, "")
|
||||||
|
normalized = normalized.replace(/^\/+/, "")
|
||||||
|
normalized = normalized.replace(/^public\//, "")
|
||||||
|
|
||||||
|
const hasValidBase = VALID_IMAGE_BASES.some((base) =>
|
||||||
|
normalized.startsWith(`${base}/`)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasValidBase) {
|
||||||
|
const isJustFilename = !normalized.includes("/")
|
||||||
|
if (isJustFilename && slug) {
|
||||||
|
normalized = `${defaultBasePath}/${slug}/${normalized}`
|
||||||
|
} else {
|
||||||
|
normalized = `${defaultBasePath}/${normalized}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `/${normalized}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeContentImagePaths(
|
||||||
|
content: string,
|
||||||
|
defaultBasePath: string = "articles",
|
||||||
|
slug?: string
|
||||||
|
): string {
|
||||||
const markdownImageRegex = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
|
const markdownImageRegex = /!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g
|
||||||
content = content.replace(markdownImageRegex, (match, alt, imagePath) => {
|
content = content.replace(markdownImageRegex, (match, alt, imagePath) => {
|
||||||
const normalized = normalizeImagePath(imagePath)
|
const normalized = normalizeImagePath(imagePath, defaultBasePath, slug)
|
||||||
return ``
|
return ``
|
||||||
})
|
})
|
||||||
|
|
||||||
// Match HTML img tags: <img src="path" /> or <img src='path' />
|
|
||||||
const htmlImageRegex = /(<img\s+[^>]*src=)(["'])([^"']+)\2([^>]*>)/gi
|
const htmlImageRegex = /(<img\s+[^>]*src=)(["'])([^"']+)\2([^>]*>)/gi
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
htmlImageRegex,
|
htmlImageRegex,
|
||||||
(match, prefix, quote, imagePath, suffix) => {
|
(match, prefix, quote, imagePath, suffix) => {
|
||||||
const normalized = normalizeImagePath(imagePath)
|
const normalized = normalizeImagePath(imagePath, defaultBasePath, slug)
|
||||||
return `${prefix}${quote}${normalized}${quote}${suffix}`
|
return `${prefix}${quote}${normalized}${quote}${suffix}`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -103,7 +96,6 @@ export interface Article {
|
|||||||
const articlesDirectory = path.join(process.cwd(), "content/articles")
|
const articlesDirectory = path.join(process.cwd(), "content/articles")
|
||||||
const projectsDirectory = path.join(process.cwd(), "content/projects")
|
const projectsDirectory = path.join(process.cwd(), "content/projects")
|
||||||
|
|
||||||
// Generic function to read and process markdown content from any directory
|
|
||||||
export function getMarkdownContent<T = any>(options: {
|
export function getMarkdownContent<T = any>(options: {
|
||||||
directory: string
|
directory: string
|
||||||
excludeFiles?: string[]
|
excludeFiles?: string[]
|
||||||
@@ -120,23 +112,18 @@ export function getMarkdownContent<T = any>(options: {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read markdown file as string
|
|
||||||
const fullPath = path.join(directory, fileName)
|
const fullPath = path.join(directory, fileName)
|
||||||
const fileContents = fs.readFileSync(fullPath, "utf8")
|
const fileContents = fs.readFileSync(fullPath, "utf8")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use matter with options to handle multiline strings
|
|
||||||
const matterResult = matter(fileContents, {
|
const matterResult = matter(fileContents, {
|
||||||
engines: {
|
engines: {
|
||||||
yaml: {
|
yaml: {
|
||||||
// Ensure multiline strings are parsed correctly
|
|
||||||
parse: (str: string) => {
|
parse: (str: string) => {
|
||||||
try {
|
try {
|
||||||
// Use js-yaml's safe load to parse the YAML with type assertion
|
|
||||||
return jsYaml.load(str) as object
|
return jsYaml.load(str) as object
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error parsing frontmatter in ${fileName}:`, e)
|
console.error(`Error parsing frontmatter in ${fileName}:`, e)
|
||||||
// Fallback to empty object if parsing fails
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -144,12 +131,10 @@ export function getMarkdownContent<T = any>(options: {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Use custom processor if provided
|
|
||||||
if (processContent) {
|
if (processContent) {
|
||||||
return processContent(matterResult.data, matterResult.content, id)
|
return processContent(matterResult.data, matterResult.content, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default processing - return raw data with content
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
...matterResult.data,
|
...matterResult.data,
|
||||||
@@ -157,7 +142,6 @@ export function getMarkdownContent<T = any>(options: {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error processing ${fileName}:`, error)
|
console.error(`Error processing ${fileName}:`, error)
|
||||||
// Return minimal content data if there's an error
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title: `Error processing ${id}`,
|
title: `Error processing ${id}`,
|
||||||
@@ -181,13 +165,11 @@ export function getArticles(options?: {
|
|||||||
directory: articlesDirectory,
|
directory: articlesDirectory,
|
||||||
excludeFiles: ["readme", "_readme", "_article-template"],
|
excludeFiles: ["readme", "_readme", "_article-template"],
|
||||||
processContent: (data, content, id) => {
|
processContent: (data, content, id) => {
|
||||||
// Normalize tags from both 'tags' and 'tag' fields
|
|
||||||
const rawTags = [
|
const rawTags = [
|
||||||
...(Array.isArray(data?.tags) ? data.tags : []),
|
...(Array.isArray(data?.tags) ? data.tags : []),
|
||||||
...(data?.tag ? [data.tag] : []),
|
...(data?.tag ? [data.tag] : []),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Process and normalize tags
|
|
||||||
const normalizedTags = rawTags
|
const normalizedTags = rawTags
|
||||||
.map((tag) => {
|
.map((tag) => {
|
||||||
if (typeof tag !== "string") return null
|
if (typeof tag !== "string") return null
|
||||||
@@ -206,15 +188,15 @@ export function getArticles(options?: {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
...data,
|
...data,
|
||||||
|
image: normalizeImagePath(data.image, "articles", id),
|
||||||
tags: normalizedTags,
|
tags: normalizedTags,
|
||||||
content: normalizeContentImagePaths(content, "articles"),
|
content: normalizeContentImagePaths(content, "articles", id),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let filteredArticles = allArticles
|
let filteredArticles = allArticles
|
||||||
|
|
||||||
// Filter by tag if provided
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
const tagId = tag.toLowerCase()
|
const tagId = tag.toLowerCase()
|
||||||
filteredArticles = filteredArticles.filter((article) =>
|
filteredArticles = filteredArticles.filter((article) =>
|
||||||
@@ -222,7 +204,6 @@ export function getArticles(options?: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by project if provided
|
|
||||||
if (project) {
|
if (project) {
|
||||||
filteredArticles = filteredArticles.filter(
|
filteredArticles = filteredArticles.filter(
|
||||||
(article) =>
|
(article) =>
|
||||||
@@ -230,13 +211,10 @@ export function getArticles(options?: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort posts by date
|
|
||||||
return filteredArticles
|
return filteredArticles
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const dateA = new Date(a.date)
|
const dateA = new Date(a.date)
|
||||||
const dateB = new Date(b.date)
|
const dateB = new Date(b.date)
|
||||||
|
|
||||||
// Sort in descending order (newest first)
|
|
||||||
return dateB.getTime() - dateA.getTime()
|
return dateB.getTime() - dateA.getTime()
|
||||||
})
|
})
|
||||||
.slice(0, limit)
|
.slice(0, limit)
|
||||||
@@ -255,21 +233,19 @@ export function getProjects(options?: {
|
|||||||
processContent: (data, content, id) => ({
|
processContent: (data, content, id) => ({
|
||||||
id,
|
id,
|
||||||
...data,
|
...data,
|
||||||
content: normalizeContentImagePaths(content, "projects"),
|
image: normalizeImagePath(data.image, "projects", id),
|
||||||
|
content: normalizeContentImagePaths(content, "projects", id),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Filter by tag if provided
|
|
||||||
if (tag) {
|
if (tag) {
|
||||||
allProjects = allProjects.filter((project) => project.tags?.includes(tag))
|
allProjects = allProjects.filter((project) => project.tags?.includes(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by status if provided
|
|
||||||
if (status) {
|
if (status) {
|
||||||
allProjects = allProjects.filter((project) => project.status === status)
|
allProjects = allProjects.filter((project) => project.status === status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply limit if provided
|
|
||||||
if (limit && limit > 0) {
|
if (limit && limit > 0) {
|
||||||
allProjects = allProjects.slice(0, limit)
|
allProjects = allProjects.slice(0, limit)
|
||||||
}
|
}
|
||||||
@@ -288,7 +264,6 @@ export const getArticleTags = () => {
|
|||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Sort tags alphabetically by name
|
|
||||||
return allTags.sort((a, b) => a.name.localeCompare(b.name))
|
return allTags.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user