A Self-Built Static Demo Publisher That Treats Releases as Pointer Switches
Recently, my professional state has been a bit like a browser with too many tabs open.
I used to be a frontend developer, then started working on the backend, gradually becoming a full-stack engineer. Later, I led a team and began to care about R&D processes, project collaboration, and delivery quality. Recently, due to business needs, I started studying product design, user flows, and a very specific problem:
When a client wants to see a web page effect, how do you give them a truly usable link?
It sounds simple. Compress the project, upload it to a platform, and send the link to the client.
I did seriously consider third-party static hosting platforms. They are usually quick to get started with, but for temporary client demos, there are some practical issues: projects need to be switched between multiple platforms, and the configuration and upload processes are not unified; some pages display platform ads or branding; after handing project files and access permissions to an external platform, the security boundary also needs extra evaluation. Rules regarding free quotas, access restrictions, and custom domains can also throw a small curveball at a critical moment.
If you only demo occasionally, third-party platforms are certainly sufficient. But when demo projects start to multiply, I prefer to manage them centrally in my own backend: upload, publish, take offline, expire, and delete, all with clear statuses, and links that don't require archaeological digs through chat histories.
So I decided to build a lightweight static demo publishing system myself.
What I really wanted to solve was more than just uploading files
The system eventually formed a relatively clear flow:
Create Project
↓
Upload ZIP
↓
Extract to Temp Directory
↓
Security Check
↓
Generate Version Directory
↓
Confirm Publish
↓
Generate Demo Link
An admin creates a project in the backend, uploads a static website archive, and the system generates a random Token, for example:
https://demo.example.com/p/8f7c2a1e9d4b6c30
The client opens the link to view the page, without needing to install Node.js or care whether the project is Vue, React, or hand-written HTML.
The system's boundary is also very clear: only publish static files, execute no server-side code. HTML, CSS, JavaScript, images are fine; PHP, Java, Python backend programs are not. It is a demo system, not planning to secretly transform into a cloud server.
Frontend: Upload and publish are split into two steps
The frontend uses Vue 3. The upload function itself is not complicated, but I split "upload version" and "official publish" into two separate actions.
const formData = new FormData()
formData.append('file', selectedFile)
const version = await request.post(
'/demo/' + projectId + '/upload',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 0
}
)
// After upload succeeds, admin confirms publish
await request.post(
'/demo/' + projectId + '/v/' + version.id + '/pub'
)
Why not auto-publish after upload?
Because the scariest thing during a client demo is not an error, but thinking the publish was successful while the client sees a half-finished product.
Uploading only generates a pending version; the admin confirms it is correct before switching to the live version. This reduces operational errors and leaves room for version comparison, rollback, and release notes.
Backend: You can't just extract a ZIP file any way you want
The part of a static publishing system that needs the most serious attention is ZIP file security.
A compressed archive might contain paths like this:
../../application.properties
If you directly concatenate the path for extraction, files could be written outside the demo directory. Educational code can handle it like this:
Path target = tempDir
.resolve(entry.getName())
.normalize();
if (!target.startsWith(tempDir)) {
throw new PublishException("Illegal file path");
}
Besides path traversal, you also need to limit the ZIP size, the total size after extraction, the number of files, and allowed extensions, while also confirming that an index.html exists in the root directory or one level of subdirectory.
private static final Set<String> ALLOWED_EXTENSIONS = Set.of(
"html", "css", "js", "json",
"png", "jpg", "jpeg", "gif", "svg", "webp", "woff2"
);
Uploaded files first go into a temporary directory. After the security check passes, they are moved to the official version directory.
/data/demo/
├── projects/
│ └── project-001/
│ └── versions/
│ ├── v1/
│ └── v2/
└── temp/
The project source code directory and runtime data directory must also be separated. The ZIP files uploaded by clients, extracted files, and version directories should not be placed inside the frontend source code directory or Spring Boot's static resource directory, otherwise business data can easily be deleted by mistake during redeployment.
Publishing a version is essentially a "pointer switch"
The system does not directly overwrite the current live files. After uploading a new version, an independent directory is generated first:
versions/v1/
versions/v2/
versions/v3/
The database records which version is currently published. Publishing only updates the current version relationship:
public void publish(Long projectId, Long versionId) {
DemoProjectVersion version = versionMapper.findById(versionId);
if (version == null || !projectId.equals(version.getProjectId())) {
throw new PublishException("Version does not exist");
}
versionMapper.markPublished(versionId);
projectMapper.switchCurrentVersion(projectId, versionId);
}
This way, if a new version upload fails, the old version can still be accessed normally. The client sees a stable link, and the developer doesn't need to pray for good server health before a demo.
Nginx: Responsible for entry, HTTPS, and request forwarding
Nginx here is mainly responsible for the domain name, HTTPS, and request forwarding. Below is a simplified example configuration:
server {
listen 443 ssl http2;
server_name demo.example.com;
ssl_certificate /etc/nginx/cert/fullchain.pem;
ssl_certificate_key /etc/nginx/cert/privkey.pem;
location /demo/ {
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 60s;
}
}
Currently, the backend finds the corresponding version directory based on the Token, then returns the static file. If traffic increases later, the backend can be made responsible only for authentication and path resolution, then use Nginx's X-Accel-Redirect to send the file, reducing application thread involvement in large file transfers.
After modifying the configuration, check the syntax first, then reload the service:
nginx -t && systemctl reload nginx
This command looks simple, but it is a very practical "psychological massage" during deployment.
Expiry and taking offline cannot rely on memory
Temporary demo projects are usually not permanently valid. The system supports project offline and expiry handling, checking the current status upon access:
if (!"PUBLISHED".equals(project.getStatus())) {
return AccessResult.notFound();
}
if (project.getExpiresAt() != null
&& project.getExpiresAt().isBefore(LocalDateTime.now())) {
projectMapper.markExpired(project.getId());
return AccessResult.expired();
}
Background scheduled tasks handle expired projects and also clean up temporary upload directories that have been incomplete for too long. Otherwise, the server disk will eventually turn into a "museum of historically uploaded files."
After finishing, I realized I had built a business process
On the surface, it solves static file publishing.
In reality, the admin cares about version control, the client cares about whether the link opens, and the developer cares about path security, file boundaries, and deployment stability.
This is also a feeling that has become increasingly clear after I moved from frontend to full-stack, and then into business and product:
Technology is not about piling up features, but about turning a vague requirement into a process that runs stably.
This static demo publishing system has been completed and is online. It is not complex enough to require dozens of architecture diagrams, but it genuinely solved a small hassle at work: when a client needs to see a page, I can just send a link; when a project needs updating, I don't have to find a platform again; after the demo ends, I can take the project offline or set an expiration time.
Many systems might start from a very ordinary sentence:
"Can you send me a link to see it?"
If you have similar demo, preview, or temporary publishing needs, feel free to discuss. I will also gradually open up the entry point based on the actual situation.