mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-01-09 06:08:03 -05:00
feat: normalize image paths for main image (#589)
This commit is contained in:
@@ -8,7 +8,7 @@ tags:
|
||||
- privacy
|
||||
- user 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
|
||||
@@ -34,7 +34,7 @@ Through qualitative interviews, we identified recurring themes across all tools
|
||||
|
||||
## **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**
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
|
||||
## Thematic affinity mapping
|
||||
|
||||
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*
|
||||
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_
|
||||
|
||||
### **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.
|
||||
|
||||
**Quotes & Evidence:**
|
||||
|
||||
> “I thought shielded would mean my vote would always be private… weird that I had to hover to see details.”
|
||||
>
|
||||
|
||||

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

|
||||
|
||||
@@ -82,11 +80,11 @@ Privacy Pool Github
|
||||
|
||||
**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**
|
||||
|
||||
*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.
|
||||
- Even technically advanced users questioned how much custody or data the service retained.
|
||||
@@ -94,20 +92,16 @@ Privacy Pool Github
|
||||
**Quotes:**
|
||||
|
||||
> “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”
|
||||
>
|
||||
|
||||
> “If the last release was three months ago and not many stars, I don’t feel confident.”
|
||||
>
|
||||
|
||||

|
||||
|
||||
Railgun Github
|
||||
|
||||
> “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**
|
||||
|
||||
*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.
|
||||
- Non-technical users struggled to understand why new wallets, seeds, or denominations were needed.
|
||||
@@ -127,10 +121,8 @@ Fluidkey UI
|
||||
**Quotes:**
|
||||
|
||||
> “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?”
|
||||
>
|
||||
|
||||

|
||||
|
||||
@@ -141,7 +133,6 @@ Snapshot UI
|
||||
Snapshot UI
|
||||
|
||||
> “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**
|
||||
|
||||
*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”).
|
||||
- Defaults often undermined privacy (“Any” = public).
|
||||
@@ -162,17 +153,14 @@ Privacy Pool UI
|
||||
**Quotes:**
|
||||
|
||||
> “Defaults matter, it should default to shielded.”
|
||||
>
|
||||
|
||||
> “Where are the privacy controls? It’s just this tiny text.”
|
||||
>
|
||||
|
||||

|
||||
|
||||
Snapshot/Shutter UI
|
||||
|
||||
> “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**
|
||||
|
||||
*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.
|
||||
- Even confident users double-checked contract addresses or waited to see funds reappear.
|
||||
@@ -192,14 +180,12 @@ Flashbot UI
|
||||
**Quotes:**
|
||||
|
||||
> “There’s no testing mode. I wouldn’t send 1 ETH through something untested.”
|
||||
>
|
||||
|
||||

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

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

|
||||
|
||||
@@ -222,7 +207,7 @@ Railgun UI
|
||||
|
||||
### **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.
|
||||
- “Compliant privacy” was seen by technical users as “not real privacy.”
|
||||
@@ -230,10 +215,8 @@ Railgun UI
|
||||
**Quotes:**
|
||||
|
||||
> “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.”
|
||||
>
|
||||
|
||||

|
||||
|
||||
@@ -245,7 +228,7 @@ Privacy Pool UI
|
||||
|
||||
### **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.
|
||||
- Users appreciated inline explanations and step-by-step guidance.
|
||||
@@ -253,14 +236,12 @@ Privacy Pool UI
|
||||
**Quotes:**
|
||||
|
||||
> “A normal user probably doesn’t know what stealth addresses are, even I’m not sure I could define it.”
|
||||
>
|
||||
|
||||

|
||||
|
||||
Fluidkey UI
|
||||
|
||||
> “‘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**
|
||||
|
||||
*Behavior: Across all interviews, users consistently valued:*
|
||||
_Behavior: Across all interviews, users consistently valued:_
|
||||
|
||||
- **Transparency:** showing what’s happening
|
||||
- **Control:** ability to verify and customize
|
||||
@@ -282,32 +263,30 @@ Fluidkey UI
|
||||
**Quotes:**
|
||||
|
||||
> “Anything that makes me feel a little bit more safe is important, like links to audits, social proof.”
|
||||
>
|
||||
|
||||

|
||||
|
||||
Fluidkey UI
|
||||
|
||||
> “Older apps that have been around longer feel safer.”
|
||||
>
|
||||
|
||||
**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**
|
||||
|
||||
*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 |
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
| 3. Technical friction | Setup is complex | Simplify and guide onboarding |
|
||||
| 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 |
|
||||
| 6. Varying privacy motivation | Context-dependent needs | Offer adaptive privacy modes |
|
||||
| 7. Education & communication | Jargon-heavy UX | Layered explanations, plain language |
|
||||
| Theme | Core Pain Point | Design Opportunity |
|
||||
| ----------------------------- | ------------------------------- | ----------------------------------------- |
|
||||
| 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 |
|
||||
| 3. Technical friction | Setup is complex | Simplify and guide onboarding |
|
||||
| 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 |
|
||||
| 6. Varying privacy motivation | Context-dependent needs | Offer adaptive privacy modes |
|
||||
| 7. Education & communication | Jargon-heavy UX | Layered explanations, plain language |
|
||||
|
||||
## **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:**
|
||||
|
||||
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**
|
||||
|
||||
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**
|
||||
|
||||
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**
|
||||
|
||||
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**
|
||||
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 path from "path"
|
||||
|
||||
// Valid base paths for images in the public folder
|
||||
const VALID_IMAGE_BASES = ["articles", "projects", "project", "project-banners"]
|
||||
|
||||
/**
|
||||
* Normalizes image paths in markdown content to ensure they resolve correctly.
|
||||
* Handles various user input formats and converts them to /{basePath}/[slug]/[filename]
|
||||
* @param content - The markdown content to process
|
||||
* @param defaultBasePath - The default base path to use if none is detected (e.g., "articles" or "projects")
|
||||
*/
|
||||
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
|
||||
}
|
||||
function normalizeImagePath(
|
||||
imagePath: string | undefined,
|
||||
defaultBasePath: string = "articles",
|
||||
slug?: string
|
||||
): string | undefined {
|
||||
if (!imagePath) return undefined
|
||||
|
||||
let normalized = imagePath.trim()
|
||||
|
||||
// Remove leading ./ or ../
|
||||
normalized = normalized.replace(/^(?:\.\.?\/)+/, "")
|
||||
|
||||
// Remove leading /
|
||||
normalized = normalized.replace(/^\/+/, "")
|
||||
|
||||
// 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}`
|
||||
if (
|
||||
imagePath.startsWith("http://") ||
|
||||
imagePath.startsWith("https://") ||
|
||||
imagePath.startsWith("data:") ||
|
||||
imagePath.startsWith("#")
|
||||
) {
|
||||
return imagePath
|
||||
}
|
||||
|
||||
// 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
|
||||
content = content.replace(markdownImageRegex, (match, alt, imagePath) => {
|
||||
const normalized = normalizeImagePath(imagePath)
|
||||
const normalized = normalizeImagePath(imagePath, defaultBasePath, slug)
|
||||
return ``
|
||||
})
|
||||
|
||||
// Match HTML img tags: <img src="path" /> or <img src='path' />
|
||||
const htmlImageRegex = /(<img\s+[^>]*src=)(["'])([^"']+)\2([^>]*>)/gi
|
||||
content = content.replace(
|
||||
htmlImageRegex,
|
||||
(match, prefix, quote, imagePath, suffix) => {
|
||||
const normalized = normalizeImagePath(imagePath)
|
||||
const normalized = normalizeImagePath(imagePath, defaultBasePath, slug)
|
||||
return `${prefix}${quote}${normalized}${quote}${suffix}`
|
||||
}
|
||||
)
|
||||
@@ -103,7 +96,6 @@ export interface Article {
|
||||
const articlesDirectory = path.join(process.cwd(), "content/articles")
|
||||
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: {
|
||||
directory: string
|
||||
excludeFiles?: string[]
|
||||
@@ -120,23 +112,18 @@ export function getMarkdownContent<T = any>(options: {
|
||||
return null
|
||||
}
|
||||
|
||||
// Read markdown file as string
|
||||
const fullPath = path.join(directory, fileName)
|
||||
const fileContents = fs.readFileSync(fullPath, "utf8")
|
||||
|
||||
try {
|
||||
// Use matter with options to handle multiline strings
|
||||
const matterResult = matter(fileContents, {
|
||||
engines: {
|
||||
yaml: {
|
||||
// Ensure multiline strings are parsed correctly
|
||||
parse: (str: string) => {
|
||||
try {
|
||||
// Use js-yaml's safe load to parse the YAML with type assertion
|
||||
return jsYaml.load(str) as object
|
||||
} catch (e) {
|
||||
console.error(`Error parsing frontmatter in ${fileName}:`, e)
|
||||
// Fallback to empty object if parsing fails
|
||||
return {}
|
||||
}
|
||||
},
|
||||
@@ -144,12 +131,10 @@ export function getMarkdownContent<T = any>(options: {
|
||||
},
|
||||
})
|
||||
|
||||
// Use custom processor if provided
|
||||
if (processContent) {
|
||||
return processContent(matterResult.data, matterResult.content, id)
|
||||
}
|
||||
|
||||
// Default processing - return raw data with content
|
||||
return {
|
||||
id,
|
||||
...matterResult.data,
|
||||
@@ -157,7 +142,6 @@ export function getMarkdownContent<T = any>(options: {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${fileName}:`, error)
|
||||
// Return minimal content data if there's an error
|
||||
return {
|
||||
id,
|
||||
title: `Error processing ${id}`,
|
||||
@@ -181,13 +165,11 @@ export function getArticles(options?: {
|
||||
directory: articlesDirectory,
|
||||
excludeFiles: ["readme", "_readme", "_article-template"],
|
||||
processContent: (data, content, id) => {
|
||||
// Normalize tags from both 'tags' and 'tag' fields
|
||||
const rawTags = [
|
||||
...(Array.isArray(data?.tags) ? data.tags : []),
|
||||
...(data?.tag ? [data.tag] : []),
|
||||
]
|
||||
|
||||
// Process and normalize tags
|
||||
const normalizedTags = rawTags
|
||||
.map((tag) => {
|
||||
if (typeof tag !== "string") return null
|
||||
@@ -206,15 +188,15 @@ export function getArticles(options?: {
|
||||
return {
|
||||
id,
|
||||
...data,
|
||||
image: normalizeImagePath(data.image, "articles", id),
|
||||
tags: normalizedTags,
|
||||
content: normalizeContentImagePaths(content, "articles"),
|
||||
content: normalizeContentImagePaths(content, "articles", id),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
let filteredArticles = allArticles
|
||||
|
||||
// Filter by tag if provided
|
||||
if (tag) {
|
||||
const tagId = tag.toLowerCase()
|
||||
filteredArticles = filteredArticles.filter((article) =>
|
||||
@@ -222,7 +204,6 @@ export function getArticles(options?: {
|
||||
)
|
||||
}
|
||||
|
||||
// Filter by project if provided
|
||||
if (project) {
|
||||
filteredArticles = filteredArticles.filter(
|
||||
(article) =>
|
||||
@@ -230,13 +211,10 @@ export function getArticles(options?: {
|
||||
)
|
||||
}
|
||||
|
||||
// Sort posts by date
|
||||
return filteredArticles
|
||||
.sort((a, b) => {
|
||||
const dateA = new Date(a.date)
|
||||
const dateB = new Date(b.date)
|
||||
|
||||
// Sort in descending order (newest first)
|
||||
return dateB.getTime() - dateA.getTime()
|
||||
})
|
||||
.slice(0, limit)
|
||||
@@ -255,21 +233,19 @@ export function getProjects(options?: {
|
||||
processContent: (data, content, id) => ({
|
||||
id,
|
||||
...data,
|
||||
content: normalizeContentImagePaths(content, "projects"),
|
||||
image: normalizeImagePath(data.image, "projects", id),
|
||||
content: normalizeContentImagePaths(content, "projects", id),
|
||||
}),
|
||||
})
|
||||
|
||||
// Filter by tag if provided
|
||||
if (tag) {
|
||||
allProjects = allProjects.filter((project) => project.tags?.includes(tag))
|
||||
}
|
||||
|
||||
// Filter by status if provided
|
||||
if (status) {
|
||||
allProjects = allProjects.filter((project) => project.status === status)
|
||||
}
|
||||
|
||||
// Apply limit if provided
|
||||
if (limit && limit > 0) {
|
||||
allProjects = allProjects.slice(0, limit)
|
||||
}
|
||||
@@ -288,7 +264,6 @@ export const getArticleTags = () => {
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
// Sort tags alphabetically by name
|
||||
return allTags.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user