Edit DESIGN.md Directly in the Web Interface: From Idea to Implementation
Edit DESIGN.md Directly in the Web Interface: From Idea to Implementation
Section titled “Edit DESIGN.md Directly in the Web Interface: From Idea to Implementation”In the MonoSpecs project management system,
DESIGN.mdcarries the architectural design and technical decisions of a project. But the traditional editing workflow forces users to jump out to an external editor. That fragmented experience is like being interrupted in the middle of reading a poem: the inspiration is gone, and so is the mood. This article shares the solution we put into practice in the HagiCode project: editingDESIGN.mddirectly in the web interface, with support for importing templates from an online design site. After all, who does not enjoy the feeling of completing everything in one flow?
Background
Section titled “Background”As the core carrier of project design documents, DESIGN.md holds key information such as architecture design, technical decisions, and implementation guidance. However, the traditional editing approach requires users to switch to an external editor such as VS Code, manually locate the physical path, and then edit the file. It is not especially complicated, but after repeating the process a few times, it becomes tiring.
The problems mainly show up in the following ways:
- Fragmented workflow: users must constantly switch between the web management interface and a local editor, breaking the continuity of their workflow, much like having the music cut out in the middle of a song.
- Hard to reuse: the design site already publishes a rich library of design templates, but they cannot be integrated directly into the project editing workflow. The good stuff exists, but you still cannot use it where you need it.
- Missing experience loop: there is no closed loop for “preview-select-import,” so users must copy and paste manually, which increases the risk of mistakes.
- Collaboration friction: keeping design documents and code implementation in sync becomes a high-friction process, which hurts team efficiency.
To solve these pain points, we decided to add direct editing support for DESIGN.md in the web interface and allow one-click template import from an online design site. It was not some earth-shaking decision. We simply wanted to make the development experience smoother.
About HagiCode
Section titled “About HagiCode”The solution shared in this article comes from our hands-on experience in the HagiCode project. HagiCode is an AI-driven coding assistant project, and during development we frequently need to maintain project design documents. To help the team collaborate more efficiently, we explored and implemented this online editing and import solution. There is nothing mysterious about it. We ran into a problem and worked out a way to solve it.
Technical Solution
Section titled “Technical Solution”Overall Architecture
Section titled “Overall Architecture”This solution uses a frontend-backend separated architecture with a same-origin proxy, mainly composed of the following layers. In practice, the design can be summed up as “each part doing its own job”:
1. Frontend editor layer
// Core component: DesignMdManagementDrawer// Responsibility: handle editing, saving, version conflict detection, and import flow2. Backend service layer
// Location: repos/hagicode-core/src/PCode.Application/ProjectAppService.DesignMd.cs// Responsibility: path resolution, file read/write, and version management3. Same-origin proxy layer
// Location: repos/hagicode-core/src/PCode.Application/ProjectAppService.DesignMdSiteIndex.cs// Responsibility: proxy design site resources, preview image caching, and security validationKey Technical Decisions
Section titled “Key Technical Decisions”Decision 1: Global Drawer Pattern
Section titled “Decision 1: Global Drawer Pattern”We use a single global drawer instead of local pop-up layers, with state managed through layoutSlice, which gives users a consistent experience across views (classic and kanban). No matter which view the user opens the editor from, they get the same interaction model. A consistent experience makes people feel more at ease instead of getting disoriented when they switch views.
Decision 2: Project-Scoped API
Section titled “Decision 2: Project-Scoped API”We mounted DESIGN.md-related endpoints under ProjectController, reusing the existing project permission boundary and avoiding the complexity of adding a separate controller. This makes permission handling clearer and also aligns with RESTful resource organization. Sometimes reuse is more meaningful than creating something new from scratch.
Decision 3: Version Conflict Detection
Section titled “Decision 3: Version Conflict Detection”We derive an opaque version from the file system’s LastWriteTimeUtc, which gives us lightweight optimistic concurrency control. When multiple users edit the same file at once, the system can detect conflicts and prompt the user to refresh. This design does not block editing, while still protecting data consistency.
Decision 4: Same-Origin Proxy Pattern
Section titled “Decision 4: Same-Origin Proxy Pattern”We use IHttpClientFactory to proxy external design-site resources, avoiding both cross-origin issues and SSRF risks. This keeps the system secure while also simplifying frontend calls. You can hardly be too careful with security.
Core Implementation
Section titled “Core Implementation”1. Edit DESIGN.md Directly
Section titled “1. Edit DESIGN.md Directly”Backend Implementation
Section titled “Backend Implementation”The backend is mainly responsible for path resolution, file read/write, and version management. These tasks are basic, but indispensable, like the foundation of a house:
// Path resolution and security validationprivate Task<string> ResolveDesignDocumentDirectoryAsync(string projectPath, string? repositoryPath){ if (string.IsNullOrWhiteSpace(repositoryPath)) { return Task.FromResult(Path.GetFullPath(projectPath)); } return ValidateSubPathAsync(projectPath, repositoryPath);}
// Version generation (based on file system 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 version design is interesting in its simplicity: we use the file’s last modified time and size to generate a unique version identifier. It is lightweight and reliable, with no extra version database to maintain. Simple solutions are often the most effective.
Frontend Implementation
Section titled “Frontend Implementation”On the frontend, we implement dirty-state detection and save logic. This design helps users understand whether their changes have been saved and reduces the anxiety of “what if I lose it?”:
// Dirty-state detection and save logicconst [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]);In this implementation, we maintain two pieces of state: draft is the content currently being edited, while savedDraft is the saved content. Comparing them tells us whether there are unsaved changes. The design is simple, but it gives people peace of mind. Nobody wants the thing they worked hard on to disappear.
2. Import Design Files from an Online Source
Section titled “2. Import Design Files from an Online Source”Directory Structure
Section titled “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 pipelineIndex Data Format
Section titled “Index Data Format”The index file on the design site defines all available templates. With this index, users can choose the template they want as easily as ordering from a menu:
{ "entries": [ { "slug": "linear.app", "title": "Linear Inspired Design System", "summary": "AI Product / Dark Feel", "detailUrl": "/designs/linear.app/", "designDownloadUrl": "/designs/linear.app/DESIGN.md", "previewLightImageUrl": "...", "previewDarkImageUrl": "..." } ]}Each entry includes the template’s basic information and download links. The backend reads the list of available templates from this index and presents them for the user to choose from. That makes selection intuitive instead of forcing people to feel their way around in the dark.
Same-Origin Proxy Implementation
Section titled “Same-Origin Proxy Implementation”To keep things secure, the backend performs strict validation on access to the design site. You cannot be too cautious about security:
// Safe slug validationprivate 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 temp 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 do two things here: first, we validate the slug format strictly with a regular expression to prevent path traversal attacks; second, we cache preview images to reduce pressure on the external site. The former is protection, the latter is optimization, and both matter.
3. Full Import Flow
Section titled “3. Full Import Flow”// 1. Open the import drawerconst handleRequestImportDrawer = useCallback(() => { setIsImportDrawerOpen(true);}, []);
// 2. Select and importconst handleImportRequest = useCallback((entry) => { if (isDirty) { setPendingImportEntry(entry); setConfirmMode('import'); // overwrite confirmation return; } void executeImport(entry);}, [isDirty]);
// 3. Execute importconst executeImport = useCallback(async (entry) => { const result = await getProjectDesignMdSiteImportDocument( activeTarget.projectId, entry.slug ); setDraft(result.content); // replace editor text only, do not save automatically setIsImportDrawerOpen(false);}, [activeTarget?.projectId]);The import flow follows a “user confirmation” principle: after import, only the editor content is updated, and nothing is saved automatically. Users can inspect the imported content and save it manually only after confirming it looks right. The final decision should stay in the hands of the user.
Practical Examples
Section titled “Practical Examples”Scenario 1: Creating DESIGN.md in the Project Root
Section titled “Scenario 1: Creating DESIGN.md in the Project Root”When DESIGN.md does not exist, the backend returns a virtual document state. This lets the frontend avoid special handling for the “file does not exist” case, and a unified API simplifies the code logic:
return new ProjectDesignDocumentDto{ Path = targetPath, Exists = false, // virtual document state Content = string.Empty, Version = null};
// Automatically create the file on first savepublic async Task<SaveProjectDesignDocumentResultDto> SaveDesignDocumentAsync(...){ Directory.CreateDirectory(targetDirectory); await File.WriteAllTextAsync(targetPath, input.Content); return new SaveProjectDesignDocumentResultDto { Created = !exists };}The benefit of this design is that the frontend does not need special-case logic for missing files. By hiding that complexity in the backend, the frontend can focus more easily on user experience.
Scenario 2: Import a Template from the Design Site
Section titled “Scenario 2: Import a Template from the Design Site”After the user selects the “Linear” design template in the import drawer, the system fetches the DESIGN.md content through the backend proxy. The whole process is transparent to the user: they only choose a template, and the system handles the network requests and data transformation automatically.
// 1. The system fetches DESIGN.md content through the backend proxyGET /api/project/{id}/design-md/site-index/linear.app
// 2. The backend validates the slug and fetches content from upstreamvar entry = FindDesignSiteEntry(catalog, "linear.app");using var upstreamResponse = await httpClient.SendAsync(request);var content = await upstreamResponse.Content.ReadAsStringAsync();
// 3. The frontend replaces the editor textsetDraft(result.content);// The user reviews it and then saves it manually to diskThe whole flow stays transparent to the user. They just choose a template, and the system handles the networking and transformation behind the scenes. That is the experience we want: simple, but powerful.
Scenario 3: Handling Version Conflicts
Section titled “Scenario 3: Handling Version Conflicts”When multiple users edit the same DESIGN.md at the same time, the system detects version conflicts. This optimistic concurrency control mechanism preserves data consistency without blocking the user’s edits:
if (!string.Equals(currentVersion, expectedVersion, StringComparison.Ordinal)){ throw new BusinessException( ProjectDesignDocumentErrorCodes.VersionConflict, $"DESIGN.md at '{targetPath}' changed on disk.");}The frontend catches this error and prompts the user:
// Frontend prompts the user to refresh and retry<Alert> <AlertTitle>Version conflict</AlertTitle> <AlertDescription> The file was modified by another process. Please refresh to get the latest version and try again. </AlertDescription></Alert>This optimistic concurrency control mechanism keeps data consistent without blocking users while they work. Conflicts are unavoidable, but at least users should know what happened instead of silently losing their changes.
Notes and Best Practices
Section titled “Notes and Best Practices”1. Path Security
Section titled “1. Path Security”Always validate repositoryPath to prevent path traversal attacks. You can never do too much when it comes to security:
// Always validate repositoryPath to prevent path traversal attacksreturn ValidateSubPathAsync(projectPath, repositoryPath);// Reject dangerous inputs such as "../" and absolute paths2. Cache Strategy
Section titled “2. Cache Strategy”Cache preview images for 24 hours, with a maximum of 160 files. Moderate caching improves performance, but balance still matters:
// Cache preview images for 24 hours, with a maximum of 160 filesprivate static readonly TimeSpan PreviewCacheTtl = TimeSpan.FromHours(24);private const int PreviewCacheMaxFiles = 160;// Periodically clean up expired cache3. Error Handling
Section titled “3. Error Handling”Gracefully degrade when the upstream site is unavailable. This design ensures that even if an external dependency fails, the core editing functionality still works normally:
// Gracefully degrade when the upstream site is unavailabletry { const catalog = await getProjectDesignMdSiteImportCatalog(projectId);} catch (error) { toast.error(t('project.designMd.siteImport.feedback.catalogLoadFailed')); // The main editing drawer remains available}This graceful degradation ensures that even when external dependencies are unavailable, the core editing function continues to work. A system should be resilient instead of collapsing the moment something goes wrong.
4. User Experience Optimization
Section titled “4. User Experience Optimization”Confirm overwrites before importing, and do not save automatically after import. Users should stay in control of their own actions:
// Confirm overwrite before importif (isDirty) { setConfirmMode('import'); return;}
// Do not save automatically after import; let the user confirmsetDraft(result.content); // update draft only// The content is written to disk only after the user reviews it and clicks Save5. Performance Considerations
Section titled “5. Performance Considerations”Use an HTTP client factory to avoid creating too many connections. Resource management may seem small, but doing it well can make a big difference:
// Use an HTTP client factory to avoid creating too many connectionsprivate const string DesignSiteProxyClientName = "ProjectDesignSiteProxy";private static readonly TimeSpan DesignSiteProxyTimeout = TimeSpan.FromSeconds(8);Suggested Extensions
Section titled “Suggested Extensions”- Markdown enhancement: we currently use a basic
Textarea, but we could upgrade to CodeMirror for syntax highlighting and keyboard shortcuts. When the editor feels better, writing documentation feels better too. - Preview mode: add real-time Markdown preview to improve the editing experience. What-you-see-is-what-you-get always gives people more confidence.
- Diff merge: implement an intelligent merge algorithm instead of simple full-text replacement. Conflicts are inevitable, but the conflict-resolution process does not have to be painful.
- Local caching: cache
design.jsonin the database to reduce dependency on the external site. The fewer dependencies a system has, the more stable it tends to be.
Summary
Section titled “Summary”In the HagiCode project, we implemented a complete online editing and import solution for DESIGN.md through frontend-backend collaboration. The core value of this solution lies in the following points:
- Higher efficiency: no need to switch tools; editing and importing design documents can happen in one unified web interface.
- Lower barrier to entry: one-click design template import helps new projects get started quickly.
- Secure and reliable: path validation, version conflict detection, and graceful degradation mechanisms keep the system stable.
- Better user experience: the global drawer, dirty-state detection, and confirmation dialogs refine the overall interaction experience.
This solution is already running in the HagiCode project and has solved the team’s pain points around design document management. If you are facing similar problems, I hope this article gives you some useful ideas. There is no particularly profound theory here, only the practical work of running into a problem and finding a way to solve it.
References
Section titled “References”- HagiCode project repository: github.com/HagiCode-org/site
- HagiCode official website: hagicode.com
- MonoSpecs project management system: docs.hagicode.com
- 30-minute hands-on demo: www.bilibili.com/video/BV1pirZBuEzq/
- Docker Compose installation guide: docs.hagicode.com/installation/docker-compose
- Desktop installation: hagicode.com/desktop/
If this article helped you, feel free to give the project a Star on GitHub. The public beta has already started, and you can join the experience right after installing it. Open-source projects always need more feedback and encouragement, and if you found this useful, it is worth helping more people discover it.
“Beautiful things or people do not have to belong to you. As long as they remain beautiful, it is enough to quietly appreciate that beauty.”
The same goes for a DESIGN.md editor. It does not need to be overly complex. If it helps you work efficiently, that is already enough.
Copyright Notice
Section titled “Copyright Notice”Thank you for reading. If you found this article useful, please consider liking, bookmarking, and sharing it. This content was created with AI-assisted collaboration, and the final version was reviewed and approved by the author.
- Author: newbe36524
- Original article: https://docs.hagicode.com/blog/2026-04-09-design-md-web-editor-implementation/
- Copyright statement: Unless otherwise noted, all articles on this blog are licensed under BY-NC-SA. Please include attribution when reposting!