In the MonoSpecs project management ecosystem, DESIGN.md serves as the cornerstone for architectural design documentation and critical technical decision-making. However, the traditional editing workflow demands users to constantly switch to external editors—a fragmented process that disrupts creative flow much like having your favorite song interrupted by an unexpected commercial break. This article presents our comprehensive solution implemented in the HagiCode project: enabling direct DESIGN.md editing within the web interface, complete with one-click template import from our online design repository. After all, who doesn't appreciate a seamless, uninterrupted workflow?

Background and Problem Statement

DESIGN.md functions as the primary vessel for project design documentation,承载着 architecture blueprints, technical decisions, and implementation guidelines. Yet the conventional editing approach forces users into a tedious cycle: navigate to the web management interface, locate the physical file path, switch to an external editor like VS Code, make changes, save, and return. While individually simple, this repetitive context-switching accumulates into significant productivity drain.

The specific pain points manifest in several critical dimensions:

  • Workflow Fragmentation: Users must constantly toggle between the web management interface and local editors, shattering workflow continuity. It's akin to trying to have a conversation while someone keeps changing the radio station—the rhythm is completely lost.
  • Template Reusability Challenges: Our design repository hosts a rich library of templates, yet integrating them into the project editing workflow proves unnecessarily difficult. It's frustrating having excellent resources available but being unable to leverage them efficiently.
  • Missing Preview-Import Loop: The absence of a "preview-select-import"闭环 forces manual copy-paste operations, inherently increasing error probability. The more manual steps involved, the higher the likelihood of mistakes creeping in.
  • Collaboration Friction: Synchronizing design documents with code implementation becomes unnecessarily high-friction, impeding team collaboration efficiency. Team collaboration is already challenging enough without adding artificial barriers.

To address these pain points, we committed to implementing direct DESIGN.md editing capabilities within the web interface, coupled with one-click template import functionality. This isn't revolutionary innovation—it's simply about making the development experience smoother and more intuitive.

About HagiCode

The solution presented here stems from our practical experience with the HagiCode project. HagiCode is an AI-driven code assistant where we needed to frequently maintain project design documentation. To enable more efficient team collaboration, we explored and implemented this online editing and import solution. Nothing particularly groundbreaking—just identifying problems and finding practical solutions.

Technical Architecture

Overall System Design

Our solution employs a same-origin proxy architecture with clear separation between frontend and backend layers. The design philosophy is straightforward: each layer handles what it does best.

1. Frontend Editor Layer

  • Core Component: DesignMdManagementDrawer
  • Location: repos/web/src/components/project/DesignMdManagementDrawer.tsx
  • Responsibilities: Editing interface, save operations, version conflict detection, import workflow orchestration

2. Backend Service Layer

  • Core Service: ProjectAppService.DesignMd
  • Location: repos/hagicode-core/src/PCode.Application/ProjectAppService.DesignMd.cs
  • Responsibilities: Path resolution, file I/O operations, version management

3. Same-Origin Proxy Layer

  • Proxy Service: ProjectAppService.DesignMdSiteIndex
  • Location: repos/hagicode-core/src/PCode.Application/ProjectAppService.DesignMdSiteIndex.cs
  • Responsibilities: Proxying design site resources, preview image caching, security validation

Key Technical Decisions

Decision 1: Global Drawer Pattern

We adopted a single global drawer rather than local modals, managing state through layoutSlice to ensure consistent experience across different views (classic/kanban). This approach guarantees users receive uniform interaction patterns regardless of which view they're working in. Consistency breeds comfort—users shouldn't feel disoriented simply because they switched views.

Decision 2: Project-Scoped API

DESIGN.md related endpoints hang under ProjectController, reusing existing project permission boundaries rather than creating new independent controllers. This design yields clearer permission management and aligns with RESTful resource organization principles. Sometimes, thoughtful reuse proves more valuable than creating something new from scratch.

Decision 3: Version Conflict Detection

We implemented lightweight optimistic concurrency control by deriving opaque version strings from filesystem LastWriteTimeUtc. When multiple users simultaneously edit the same file, the system detects conflicts and prompts users to refresh. This design neither blocks editing operations nor compromises data consistency—much like healthy interpersonal boundaries: neither too distant nor overstepping.

Decision 4: Same-Origin Proxy Pattern

By proxying external design site resources through IHttpClientFactory, we eliminated cross-origin issues and SSRF risks. This design ensures security while simplifying frontend calls. When it comes to security, one can never be too cautious—data security resembles health; you only regret neglecting it after it's lost.

Core Implementation Details

1. Direct DESIGN.md Editing

Backend Implementation

The backend primarily handles path resolution, file I/O, and version management. While foundational, these operations are indispensable—like a building's foundation:

// Path resolution and security validation
private Task<string> ResolveDesignDocumentDirectoryAsync(
    string projectPath, 
    string? repositoryPath)
{
    if (string.IsNullOrWhiteSpace(repositoryPath))
    {
        return Task.FromResult(Path.GetFullPath(projectPath));
    }
    return ValidateSubPathAsync(projectPath, repositoryPath);
}

// Version string generation (based on filesystem timestamp and size)
private static string BuildDesignDocumentVersion(string path)
{
    var fileInfo = new FileInfo(path);
    fileInfo.Refresh();
    return string.Create(
        CultureInfo.InvariantCulture,
        $"{fileInfo.LastWriteTimeUtc.Ticks:x}-{fileInfo.Length:x}");
}

The versioning design is particularly elegant: we generate a unique version identifier using the file's last modification time and size. This approach is both lightweight and reliable, eliminating the need for maintaining a separate version database. Sometimes, simpler solutions prove more effective.

Frontend Implementation

The frontend implements dirty state detection and save logic. This design keeps users informed about their modification status, reducing anxiety about potential data loss:

// Dirty state detection and save logic
const [draft, setDraft] = useState('');
const [savedDraft, setSavedDraft] = useState('');
const isDirty = draft !== savedDraft;

const handleSave = useCallback(async () => {
    const result = await saveProjectDesignMdDocument({
        ...activeTarget,
        content: draft,
        expectedVersion: document.version, // Optimistic concurrency control
    });
    setSavedDraft(draft); // Update saved state
}, [activeTarget, document, draft]);

This implementation maintains two states: draft represents current editing content, while savedDraft holds persisted content. Comparing these determines whether unsaved modifications exist. Though simple, this design provides peace of mind—nobody wants their hard work to vanish unexpectedly.

2. Importing Design Files from Online Repository

Directory Structure

repos/index/
└── src/data/public/design.json # Design template index

repos/awesome-design-md-site/
├── vendor/awesome-design-md/ # Upstream design templates
│   └── design-md/
│       ├── clickhouse/
│       │   └── DESIGN.md
│       ├── linear/
│       │   └── DESIGN.md
│       └── ...
└── src/lib/content/
    └── awesomeDesignCatalog.ts # Content pipeline

Index Data Format

The design site's index file defines all available templates. With this index, users can select desired templates as easily as ordering from a restaurant menu:

{
  "entries": [
    {
      "slug": "linear.app",
      "title": "Linear Inspired Design System",
      "summary": "AI Product / Dark Theme",
      "detailUrl": "/designs/linear.app/",
      "designDownloadUrl": "/designs/linear.app/DESIGN.md",
      "previewLightImageUrl": "...",
      "previewDarkImageUrl": "..."
    }
  ]
}

Each entry contains template metadata and download links. The backend reads available templates from this index and presents them for user selection. This design makes selection intuitive rather than groping in darkness.

Same-Origin Proxy Implementation

To ensure security, the backend implements strict validation for design site access. Security cannot be overemphasized:

// Safe slug validation
private static readonly Regex SafeDesignSiteSlugRegex =
    new("^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,127})$", RegexOptions.Compiled);

private static string NormalizeDesignSiteSlug(string slug)
{
    var normalizedSlug = slug?.Trim() ?? string.Empty;
    if (!IsSafeDesignSiteSlug(normalizedSlug))
    {
        throw new BusinessException(
            ProjectDesignSiteIndexErrorCodes.InvalidSlug,
            "Design site slug must be a single safe path segment.");
    }
    return normalizedSlug;
}

// Preview image caching (OS temporary directory)
private static string ComputePreviewCacheKey(
    string slug, 
    string theme, 
    string previewUrl)
{
    var raw = $"{slug}|{theme}|{previewUrl}";
    var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
    return Convert.ToHexString(bytes).ToLowerInvariant();
}

We accomplish two critical objectives here: first, using regular expressions to strictly validate slug format, preventing path traversal attacks; second, caching preview images to reduce external site request pressure. The former provides protection, the latter optimization—both indispensable.

3. Complete Import Workflow

// 1. Open import drawer
const handleRequestImportDrawer = useCallback(() => {
    setIsImportDrawerOpen(true);
}, []);

// 2. Select and import
const handleImportRequest = useCallback((entry) => {
    if (isDirty) {
        setPendingImportEntry(entry);
        setConfirmMode('import'); // Overwrite confirmation
        return;
    }
    void executeImport(entry);
}, [isDirty]);

// 3. Execute import
const executeImport = useCallback(async (entry) => {
    const result = await getProjectDesignMdSiteImportDocument(
        activeTarget.projectId,
        entry.slug
    );
    setDraft(result.content); // Only replace editor text, no auto-save
    setIsImportDrawerOpen(false);
}, [activeTarget?.projectId]);

The import workflow design follows a "user confirmation" principle: after importing, only editor content updates without automatic saving. Users can review imported content and manually save after confirmation. After all, users should maintain final decision authority over their work.

Practical Usage Scenarios

Scenario 1: Creating DESIGN.md in Project Root

When DESIGN.md doesn't exist, the backend returns a virtual document state. This design eliminates special "file not found" handling in frontend code, simplifying logic through unified API interfaces:

return new ProjectDesignDocumentDto
{
    Path = targetPath,
    Exists = false, // Virtual document state
    Content = string.Empty,
    Version = null
};

// File automatically created on first save
public async Task<SaveProjectDesignDocumentResultDto> 
    SaveDesignDocumentAsync(...)
{
    Directory.CreateDirectory(targetDirectory);
    await File.WriteAllTextAsync(targetPath, input.Content);
    return new SaveProjectDesignDocumentResultDto 
    { 
        Created = !exists 
    };
}

This design's advantage lies in eliminating special "file not found" handling in frontend code. Sometimes, hiding complexity in the backend allows frontend to focus more easily on user experience.

Scenario 2: Importing Templates from Design Site

After users select the "Linear" design template in the import drawer, the system retrieves DESIGN.md content through backend proxy. The entire process remains transparent to users—they simply select templates while the system automatically handles all network requests and data transformations:

// 1. System retrieves DESIGN.md content through backend proxy
GET /api/project/{id}/design-md/site-index/linear.app

// 2. Backend validates slug and fetches content from upstream
var entry = FindDesignSiteEntry(catalog, "linear.app");
using var upstreamResponse = await httpClient.SendAsync(request);
var content = await upstreamResponse.Content.ReadAsStringAsync();

// 3. Frontend replaces editor text
setDraft(result.content);
// User manually saves to disk after review

The entire process remains transparent to users. They simply select templates while the system automatically handles all network requests and data transformations. Users needn't concern themselves with underlying complexity—this is the experience we pursue: simple yet powerful.

Scenario 3: Version Conflict Handling

When multiple users simultaneously edit the same DESIGN.md, the system detects version conflicts. This optimistic concurrency control mechanism ensures data consistency without blocking editing operations:

if (!string.Equals(currentVersion, expectedVersion, 
    StringComparison.Ordinal))
{
    throw new BusinessException(
        ProjectDesignDocumentErrorCodes.VersionConflict,
        $"DESIGN.md at '{targetPath}' changed on disk.");
}

The frontend captures this error and prompts users:

// Frontend prompts user to refresh and retry
<Alert>
    <AlertTitle>Version Conflict</AlertTitle>
    <AlertDescription>
        File has been modified by another process. 
        Please refresh to latest version and retry.
    </AlertDescription>
</Alert>

This optimistic concurrency control mechanism ensures data consistency without blocking editing operations. Conflicts are inevitable, but at least users understand what occurred rather than silently losing modifications.

Best Practices and Considerations

1. Path Security

Always validate repositoryPath to prevent path traversal attacks. When it comes to security, one cannot be overly cautious:

// Always validate repositoryPath to prevent path traversal
return ValidateSubPathAsync(projectPath, repositoryPath);
// Reject dangerous inputs like "../", absolute paths, etc.

2. Caching Strategy

Preview images cache for 24 hours with maximum 160 files. Moderate caching improves performance without excess—balance remains key:

// Preview image cache: 24 hours TTL, max 160 files
private static readonly TimeSpan PreviewCacheTtl = 
    TimeSpan.FromHours(24);
private const int PreviewCacheMaxFiles = 160;
// Periodically clean expired cache

3. Error Handling

Implement graceful degradation when upstream sites become unavailable. This elegant degradation design ensures core editing functionality continues working even when external dependencies fail. After all, the entire system shouldn't collapse because one external service went down:

// Graceful degradation when upstream site unavailable
try {
    const catalog = await getProjectDesignMdSiteImportCatalog(projectId);
} catch (error) {
    toast.error(t('project.designMd.siteImport.feedback.catalogLoadFailed'));
    // Main editing drawer remains functional
}

This elegant degradation design ensures core editing functionality continues working even when external dependencies fail. Systems should demonstrate resilience rather than collapsing at first obstacle.

4. User Experience Optimization

Confirm overwrite before importing; don't auto-save after import. Users should maintain control over their operations rather than having the system act presumptuously:

// Confirm overwrite before import
if (isDirty) {
    setConfirmMode('import');
    return;
}

// Don't auto-save after import; let user confirm
setDraft(result.content); // Only update draft
// User clicks Save to actually write to disk after review

5. Performance Considerations

Use HTTP client factory to avoid creating excessive connections. Resource management might seem insignificant, but doing it well yields unexpected benefits:

// Use HTTP client factory to avoid excessive connections
private const string DesignSiteProxyClientName = 
    "ProjectDesignSiteProxy";
private static readonly TimeSpan DesignSiteProxyTimeout = 
    TimeSpan.FromSeconds(8);

Extension Recommendations

  • Markdown Enhancement: Currently using basic Textarea; consider upgrading to CodeMirror for syntax highlighting and keyboard shortcuts. Better editor experience improves document writing mood.
  • Preview Mode: Add real-time Markdown preview to enhance editing experience. What-you-see-is-what-you-get always inspires more confidence.
  • Diff Merge: Implement intelligent merge algorithms rather than simple full-text replacement. Conflicts are inevitable, but at least make conflict resolution less painful.
  • Local Caching: Cache design.json in database to reduce external site dependency. Fewer dependencies mean more stable systems—simple logic.

Summary

In the HagiCode project, we implemented a complete DESIGN.md online editing and import solution through frontend-backend collaboration. This solution's core value propositions include:

  • Efficiency Enhancement: No tool switching required; complete design document editing and importing within unified web interface. Time saved can be spent on more meaningful activities.
  • Lowered Barrier: One-click design template import enables new projects to start quickly. Easier beginnings increase persistence probability.
  • Reliability and Security: Path validation, version conflict detection, graceful degradation mechanisms ensure stable system operation. Stability forms the foundation; without it, everything is empty talk.
  • User Experience: Global drawer, dirty state detection, confirmation dialogs—these details polish interaction experience. Details determine success, especially applicable to user experience.

This solution has operated successfully in the HagiCode project, resolving our team's design document management pain points. If you face similar challenges, we hope this article provides inspiration. Nothing particularly profound—just identifying problems and finding solutions.

References

If this article proves helpful, welcome to give us a Star on GitHub. Public beta has begun—install now to participate in the experience. After all, open-source projects most lack feedback and encouragement. If you find it useful, why not let more people see it?

"Beautiful things or people need not be possessed; as long as they remain beautiful, one can simply appreciate their beauty."

The DESIGN.md editor is similar—it needn't be overly complex. As long as it helps you complete work efficiently, that's what matters.


This content was created with AI-assisted collaboration. Final content reviewed and confirmed by author.