<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yu Wang</title>
    <description>The latest articles on DEV Community by Yu Wang (@_a0eb4d8107cee18d04b2cb).</description>
    <link>https://dev.to/_a0eb4d8107cee18d04b2cb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4026780%2F07a0868f-df8b-4c28-bb07-bc5fae767792.jpg</url>
      <title>DEV Community: Yu Wang</title>
      <link>https://dev.to/_a0eb4d8107cee18d04b2cb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_a0eb4d8107cee18d04b2cb"/>
    <language>en</language>
    <item>
      <title>The DOCX demo was perfect. The first real contract was awful.</title>
      <dc:creator>Yu Wang</dc:creator>
      <pubDate>Wed, 22 Jul 2026 07:46:10 +0000</pubDate>
      <link>https://dev.to/_a0eb4d8107cee18d04b2cb/the-docx-demo-was-perfect-the-first-real-contract-was-awful-1ngo</link>
      <guid>https://dev.to/_a0eb4d8107cee18d04b2cb/the-docx-demo-was-perfect-the-first-real-contract-was-awful-1ngo</guid>
      <description>&lt;p&gt;The first DOCX demo looked great.&lt;/p&gt;

&lt;p&gt;One page. One font. A small table. No headers. No section breaks. I thought the hard part was done.&lt;/p&gt;

&lt;p&gt;Then a customer sent a real contract.&lt;/p&gt;

&lt;p&gt;It had missing fonts, nested tables, merged cells, numbered clauses, headers, footers, page breaks, and a signature block that had to stay in the right place. Our clean demo became useless in seconds.&lt;/p&gt;

&lt;p&gt;That failure changed how I think about Office preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  A file that opens is not a file that renders correctly
&lt;/h2&gt;

&lt;p&gt;Parsing the ZIP package is the easy part of DOCX. The ugly work starts after that.&lt;/p&gt;

&lt;p&gt;You need to answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which font metrics should be used when the original font is missing?&lt;/li&gt;
&lt;li&gt;Does a section break create a new page or only change layout rules?&lt;/li&gt;
&lt;li&gt;What happens when a table is wider than the page?&lt;/li&gt;
&lt;li&gt;How do floating images interact with text?&lt;/li&gt;
&lt;li&gt;Where does a paragraph split when line height and font fallback both change?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A browser can display the text and still get the document badly wrong.&lt;/p&gt;

&lt;p&gt;The same trap exists in every Office format.&lt;/p&gt;

&lt;p&gt;Old DOC and XLS files use binary structures that are nothing like DOCX and XLSX. PPT and PPTX need different readers. A huge spreadsheet may parse correctly and still freeze the tab when the renderer creates too many cells.&lt;/p&gt;

&lt;p&gt;So I stopped treating “Office preview” as one feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  We split the problem into real pipelines
&lt;/h2&gt;

&lt;p&gt;The commercial build has dedicated paths for DOC, DOCX, PPT, PPTX, XLS, and XLSX. The rough flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file bytes
  -&amp;gt; format probe
  -&amp;gt; format-specific input guard
  -&amp;gt; dedicated parser
  -&amp;gt; normalized document model
  -&amp;gt; layout and browser renderer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The protected distribution keeps parser entry points behind a small WASM license gate. Workers and WASM assets are served from the same origin. That matters for private networks, strict CSP rules, and deployments with no public internet access.&lt;/p&gt;

&lt;p&gt;The gate is deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signed license -&amp;gt; WASM verification -&amp;gt; viewer token -&amp;gt; parser entry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not make rendering better. It gives a business deployment a clear delivery boundary without pushing document bytes to our server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The open-source and commercial paths should not pretend to be the same
&lt;/h2&gt;

&lt;p&gt;I maintain an open-source File Viewer for broad browser-side preview. It covers many formats and is the right answer for a lot of products.&lt;/p&gt;

&lt;p&gt;But broad support and high Office fidelity are different jobs.&lt;/p&gt;

&lt;p&gt;The open-source path is great when the product needs fast preview across many file types and can accept layout differences. The commercial Office path exists for teams that need private deployment, source delivery, legacy Office support, and someone responsible when a release breaks.&lt;/p&gt;

&lt;p&gt;Hiding that difference would be dishonest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our test files got uglier on purpose
&lt;/h2&gt;

&lt;p&gt;Pretty samples produce pretty lies.&lt;/p&gt;

&lt;p&gt;The useful test set contains files that are unpleasant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a contract with several section changes;&lt;/li&gt;
&lt;li&gt;a DOC file saved by an old Office version;&lt;/li&gt;
&lt;li&gt;a spreadsheet with merged regions, charts, frozen panes, and a large used range;&lt;/li&gt;
&lt;li&gt;a deck with custom fonts and shapes positioned outside the normal slide box;&lt;/li&gt;
&lt;li&gt;files with missing fonts;&lt;/li&gt;
&lt;li&gt;files that are valid but unusually large.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We compare structure, page flow, cell position, and failure behavior. A renderer must also fail clearly. A blank page or a tab that spins forever is worse than an honest error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser-side preview still has limits
&lt;/h2&gt;

&lt;p&gt;This is not Microsoft Office rebuilt in JavaScript.&lt;/p&gt;

&lt;p&gt;Font substitution can move text. Rare drawing objects can look wrong. Very large files can consume enough memory to freeze the tab. Some products should still use server-side conversion for the hardest documents.&lt;/p&gt;

&lt;p&gt;The useful question is not “Can the browser render Office files?”&lt;/p&gt;

&lt;p&gt;It can.&lt;/p&gt;

&lt;p&gt;The real question is where your product draws the line between instant local preview, high-fidelity browser rendering, and server conversion.&lt;/p&gt;

&lt;p&gt;I built Flyfish Office Preview for the middle path: higher Office fidelity, private deployment, and no forced upload to our server.&lt;/p&gt;

&lt;p&gt;You can test the current demo here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.flyfish.group/" rel="noopener noreferrer"&gt;https://demo.flyfish.group/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What breaks first in your product: pagination, legacy formats, missing fonts, or spreadsheet memory?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>architecture</category>
      <category>performance</category>
    </item>
    <item>
      <title>Self-Hosting a Browser File Viewer Under a Strict CSP: Workers, WASM, and Assets Without Public CDNs</title>
      <dc:creator>Yu Wang</dc:creator>
      <pubDate>Tue, 14 Jul 2026 01:44:30 +0000</pubDate>
      <link>https://dev.to/_a0eb4d8107cee18d04b2cb/self-hosting-a-browser-file-viewer-under-a-strict-csp-workers-wasm-and-assets-without-public-cdns-4hak</link>
      <guid>https://dev.to/_a0eb4d8107cee18d04b2cb/self-hosting-a-browser-file-viewer-under-a-strict-csp-workers-wasm-and-assets-without-public-cdns-4hak</guid>
      <description>&lt;p&gt;File Viewer began with a consumer-facing application whose backend resources were limited. In 2022, my manager asked me to move document preview entirely into the frontend. That constraint eventually grew into an Apache-2.0 viewer ecosystem, and Worker/WASM delivery became one of its hardest practical deployment problems.&lt;/p&gt;

&lt;p&gt;The hardest part has been making many format engines behave consistently while keeping large customer files usable. I spent a lot of time on Worker loading, asset paths, and performance so heavy parsers remain practical in real deployments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvu61y00hixd60igt8j1f.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvu61y00hixd60igt8j1f.gif" alt="File Viewer previewing an English DOCX sample entirely in the browser" width="760" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;File Viewer previewing an English DOCX sample in the browser. Renderer chunks, Workers, WASM modules, and fonts can all be served from infrastructure you control.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A browser-side file viewer is often selected for privacy: a document can be parsed and rendered without first being uploaded to a third-party conversion service.&lt;/p&gt;

&lt;p&gt;That is only half of the deployment story.&lt;/p&gt;

&lt;p&gt;A modern viewer may still load renderer chunks, Web Workers, WASM modules, fonts, and vendor assets at runtime. If those resources come from a public CDN, the deployment is not truly self-contained. If their URLs are not planned in advance, a strict Content Security Policy can also turn a successful local integration into a production failure.&lt;/p&gt;

&lt;p&gt;This article describes the model I use for self-hosting File Viewer, an Apache-2.0 browser-native viewer for internal and private web applications. The same principles apply to many JavaScript applications that combine lazy imports, Workers, WASM, and private file URLs.&lt;/p&gt;

&lt;p&gt;The examples use v2.2.2. Exact asset filenames can change between releases, so pin a version and use the asset manifest produced by that release instead of copying filenames from an old deployment.&lt;/p&gt;

&lt;h4&gt;
  
  
  Start with the runtime graph, not the component tag
&lt;/h4&gt;

&lt;p&gt;The visible component is only the first node in the runtime graph.&lt;/p&gt;

&lt;p&gt;A complete deployment may include:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The host application and framework wrapper.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A core viewer package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Renderer bundles loaded only when a matching file is opened.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Worker scripts for pipelines such as PDF, DOCX, PPTX, spreadsheets, or archives.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WASM modules used by archive, CAD, and Typst-related paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fonts and other vendor assets.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The source file being previewed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This distinction matters because loading the initial JavaScript successfully does not prove that the deployment works. A PDF may open while a DOCX Worker is blocked. A ZIP file may fall back to a limited compatibility path because its Worker found the wrong WASM URL. A Typst document may fail only after the compiler module or font assets are requested.&lt;/p&gt;

&lt;p&gt;File Viewer currently maps 206 extensions to 24 preview pipelines. That is a routing and capability matrix, not a claim that all 206 extensions use the same renderer or provide identical fidelity.&lt;/p&gt;

&lt;p&gt;For deployment testing, think in pipelines rather than extensions.&lt;/p&gt;

&lt;h4&gt;
  
  
  Prefer one controlled origin
&lt;/h4&gt;

&lt;p&gt;The simplest strict-CSP arrangement is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.internal/
  app JavaScript and CSS
  /static/file-viewer/v2.2.2/
  /api/files/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the application, viewer assets, and file endpoint on the same origin, most runtime requests can remain under &lt;code&gt;'self'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A separate private static origin also works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://app.example.internal/
https://assets.example.internal/file-viewer/v2.2.2/
https://files.example.internal/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, this requires three configurations to agree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The viewer must generate the correct asset URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CSP must allow the asset and file origins under the appropriate directives.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The static and file servers must return suitable CORS and content-type headers.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same-origin hosting removes an entire class of deployment errors, so I use it unless a private CDN is operationally necessary.&lt;/p&gt;

&lt;h4&gt;
  
  
  Pin and copy the whole asset layout
&lt;/h4&gt;

&lt;p&gt;There are several ways to integrate the viewer, and their asset behavior differs slightly.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;@file-viewer/web-full&lt;/code&gt; IIFE loads the Custom Element and controller first, then resolves heavy renderer bundles and runtime assets relative to the script URL. For this mode, mirror the complete &lt;code&gt;dist&lt;/code&gt; directory and preserve its structure. Copying only the entry JavaScript is not enough.&lt;/p&gt;

&lt;p&gt;For Vue full packages, the copied assets normally use &lt;code&gt;/file-viewer/&lt;/code&gt; as their default base. A versioned private path can be set explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;setDefaultFullAssetBaseUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@file-viewer/vue3-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;setDefaultFullAssetBaseUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/static/file-viewer/v2.2.2/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The asset-copy output includes a generated &lt;code&gt;flyfish-viewer-assets.json&lt;/code&gt;. Keep the copied directory intact and treat the manifest as the release’s source of truth.&lt;/p&gt;

&lt;p&gt;If the application uses composed packages, a Vite project can use &lt;code&gt;@file-viewer/vite-plugin&lt;/code&gt; to discover installed presets and copy runtime assets. Other build systems can publish the same assets through their own copy step.&lt;/p&gt;

&lt;p&gt;The important principle is the same in every case: do not let a bundler rename one Worker, flatten a WASM directory, and leave the application guessing where the files moved.&lt;/p&gt;

&lt;h4&gt;
  
  
  Use explicit URLs when the gateway changes paths
&lt;/h4&gt;

&lt;p&gt;A base URL is convenient, but explicit options should remain available for reverse proxies, tenant prefixes, or unusual static gateways.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;viewerOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;archive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;workerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/static/file-viewer/v2.2.2/vendor/libarchive/worker-bundle.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;wasmUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/static/file-viewer/v2.2.2/vendor/libarchive/libarchive.wasm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;docx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;workerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/static/file-viewer/v2.2.2/vendor/docx/docx.worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;workerJsZipUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/static/file-viewer/v2.2.2/vendor/docx/jszip.min.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;spreadsheet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those archive and DOCX locations reflect the documented copied layout. For PDF, Typst, CAD, and other pipelines, take the precise versioned filenames from the copied assets or generated manifest rather than inventing a path.&lt;/p&gt;

&lt;p&gt;The spreadsheet Worker is optional and disabled by default. Main-thread parsing avoids Worker startup problems in local servers, restrictive WebViews, and CSP-sensitive environments. That is a useful reminder: a Worker is an architectural choice, not automatically a requirement for every parser.&lt;/p&gt;

&lt;h4&gt;
  
  
  Build CSP from observed resource types
&lt;/h4&gt;

&lt;p&gt;Start with &lt;code&gt;Content-Security-Policy-Report-Only&lt;/code&gt;, exercise the required pipelines, and inspect every violation before enforcing the policy.&lt;/p&gt;

&lt;p&gt;A same-origin starting point may look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Content-Security-Policy-Report-Only:
  default-src 'none';
  base-uri 'none';
  object-src 'none';
  frame-ancestors 'self';
  script-src 'self' 'wasm-unsafe-eval';
  worker-src 'self';
  connect-src 'self';
  img-src 'self' data: blob:;
  style-src 'self' 'unsafe-inline';
  font-src 'self' data:;
  media-src 'self' blob:;
  frame-src 'self';
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a starting template, not a universal policy.&lt;/p&gt;

&lt;p&gt;Several directives deserve special attention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;worker-src 'self'&lt;/code&gt; permits statically hosted same-origin Workers. Avoid adding &lt;code&gt;blob:&lt;/code&gt; automatically. Add it only if an enabled and reviewed code path actually creates blob Workers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;script-src 'wasm-unsafe-eval'&lt;/code&gt; permits WebAssembly compilation without granting general JavaScript string evaluation in browsers that support this CSP source expression. Test the exact browser fleet before deciding whether a broader exception is acceptable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;connect-src&lt;/code&gt; must include every origin used to fetch documents, renderer data, WASM, or signed file URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;img-src&lt;/code&gt;, &lt;code&gt;font-src&lt;/code&gt;, and &lt;code&gt;media-src&lt;/code&gt; depend on the document types you enable. Local uploads and generated previews can require &lt;code&gt;blob:&lt;/code&gt; or &lt;code&gt;data:&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;style-src 'unsafe-inline'&lt;/code&gt; may be needed by document layout surfaces that produce runtime styles. If the host already uses a stricter nonce- or hash-based design, validate each renderer instead of weakening the policy globally.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;frame-src&lt;/code&gt; is only necessary when using the iframe distribution or another framed preview surface.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a private asset origin, replace the relevant &lt;code&gt;'self'&lt;/code&gt; entries with the exact origin. Avoid wildcards such as &lt;code&gt;https:&lt;/code&gt; because they erase much of the value of CSP.&lt;/p&gt;

&lt;h4&gt;
  
  
  WASM failures are not always WASM failures
&lt;/h4&gt;

&lt;p&gt;When a WASM pipeline fails, first inspect the network response.&lt;/p&gt;

&lt;p&gt;A common failure sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The application requests &lt;code&gt;libarchive.wasm&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The path is missing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An SPA fallback returns &lt;code&gt;index.html&lt;/code&gt; with status 200.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The browser reports a WASM compilation or MIME error.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The underlying problem is routing, not the binary.&lt;/p&gt;

&lt;p&gt;Static asset routes should fail with a real 404 instead of falling through to the application shell. Also verify these response types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.js / .mjs    JavaScript MIME type
.wasm         application/wasm
fonts         matching font MIME type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;X-Content-Type-Options: nosniff&lt;/code&gt; after the MIME mapping is correct, not before it has been tested.&lt;/p&gt;

&lt;p&gt;The archive Worker and &lt;code&gt;libarchive.wasm&lt;/code&gt; should remain adjacent when using the default layout, unless both URLs are explicitly configured. The DOCX Worker also needs access to its JSZip dependency. A successful Worker request alone does not prove that its secondary imports succeeded.&lt;/p&gt;

&lt;h4&gt;
  
  
  Cache releases, not mutable filenames
&lt;/h4&gt;

&lt;p&gt;A versioned directory makes CSP deployment and rollback much easier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/static/file-viewer/v2.2.2/...
/static/file-viewer/v2.2.2/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Immutable versioned JavaScript, Workers, WASM, and fonts can receive long cache lifetimes. HTML entry points and any manifest used to select the current release should have shorter caching.&lt;/p&gt;

&lt;p&gt;This avoids a dangerous mixed-release state where the application loads a new renderer with an old Worker or WASM module from the browser cache.&lt;/p&gt;

&lt;p&gt;It also allows a rollback to change one application reference instead of overwriting assets in place.&lt;/p&gt;

&lt;h4&gt;
  
  
  Treat iframe mode as a separate security boundary
&lt;/h4&gt;

&lt;p&gt;The release provides a zero-dependency iframe entry for hosts that do not want an npm integration.&lt;/p&gt;

&lt;p&gt;If the iframe and parent are on different controlled origins, remember that they are separate documents with separate CSP policies. Restrict &lt;code&gt;frame-ancestors&lt;/code&gt; on the viewer origin, restrict &lt;code&gt;frame-src&lt;/code&gt; on the parent, and use an exact target origin for &lt;code&gt;postMessage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Do not use &lt;code&gt;'*'&lt;/code&gt; as the target origin for document blobs.&lt;/p&gt;

&lt;p&gt;If the parent fetches the private file and sends a &lt;code&gt;Blob&lt;/code&gt; into the iframe, the file authorization remains in the parent application. The iframe origin still needs permission to load its own Workers, WASM, fonts, and renderer chunks.&lt;/p&gt;

&lt;h4&gt;
  
  
  Test one file from every expensive pipeline
&lt;/h4&gt;

&lt;p&gt;My minimum private-deployment matrix is not 206 sample files. It is a smaller set selected to activate different runtime paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;PDF for PDF.js Worker and Range behavior.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DOCX for the document Worker and JSZip asset.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PPTX for progressive slide processing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ZIP or 7z for the archive Worker and WASM pair.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DWG for the CAD Worker and LibreDWG WASM path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Typst for compiler WASM, renderer WASM, and fonts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A local image or media file for blob URLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The iframe entry, if the application uses it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I verify:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;No request reaches an unapproved public origin.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every Worker runs under the enforced CSP.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WASM responses have the correct MIME type.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Missing assets return 404 rather than HTML.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Version rollback does not mix cached releases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The same tests work in required mobile or embedded WebViews.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CSP reports are empty after expected user actions, not merely after page load.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  The boundary of “browser-native”
&lt;/h4&gt;

&lt;p&gt;Browser-native does not mean “no network requests.” The browser still needs to fetch the source file and the assets required for its pipeline. The privacy property comes from controlling those endpoints and avoiding a mandatory document-conversion service.&lt;/p&gt;

&lt;p&gt;It also does not mean every extension has identical fidelity. Some pipelines render rich visual content, some progressively parse legacy files, and some provide safe structural inspection. Heavy CAD/BIM formats may still be better served by a deliberate conversion workflow. PlantUML rendering uses a configurable endpoint, which must be self-hosted or excluded if the environment cannot make that request.&lt;/p&gt;

&lt;p&gt;The project is a viewer, not a document editor. Download preserves the original bytes; it does not write the rendered result back into the source document. Print and HTML export are exposed only where the corresponding renderer can provide a meaningful result.&lt;/p&gt;

&lt;p&gt;Those boundaries are part of a secure design because they prevent a broad support matrix from being mistaken for a universal document engine.&lt;/p&gt;

&lt;h4&gt;
  
  
  Final thought
&lt;/h4&gt;

&lt;p&gt;A strict CSP should not be the final obstacle added after integration. It should shape the asset model from the beginning.&lt;/p&gt;

&lt;p&gt;Once Workers, WASM, fonts, renderer chunks, and source files have stable versioned locations, the policy becomes understandable. More importantly, the private-deployment promise becomes verifiable: open the network panel, exercise each pipeline, and confirm exactly where every byte came from.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/flyfish-dev/file-viewer" rel="noopener noreferrer"&gt;https://github.com/flyfish-dev/file-viewer&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Distribution guide: &lt;a href="https://doc.file-viewer.app/en/guide/distribution" rel="noopener noreferrer"&gt;https://doc.file-viewer.app/en/guide/distribution&lt;/a&gt;&lt;br&gt;&lt;br&gt;
English demo: &lt;a href="https://demo.file-viewer.app/?lang=en-US" rel="noopener noreferrer"&gt;https://demo.file-viewer.app/?lang=en-US&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Designing a Browser-Native File Preview Layer for Internal Web Apps</title>
      <dc:creator>Yu Wang</dc:creator>
      <pubDate>Mon, 13 Jul 2026 07:13:28 +0000</pubDate>
      <link>https://dev.to/_a0eb4d8107cee18d04b2cb/designing-a-browser-native-file-preview-layer-for-internal-web-apps-p0l</link>
      <guid>https://dev.to/_a0eb4d8107cee18d04b2cb/designing-a-browser-native-file-preview-layer-for-internal-web-apps-p0l</guid>
      <description>&lt;p&gt;File Viewer began with a practical constraint at work. My manager asked me to build document preview entirely in the frontend because the backend had limited resources and the product served consumers at scale. I published the first article about the approach in 2022; I did not expect so many developers to adopt it or tell me it had solved a real problem for them.&lt;/p&gt;

&lt;p&gt;The hardest part since then has been making many format engines behave consistently while keeping large customer files usable. I spent a lot of time on Worker loading, runtime asset delivery, and performance so that heavy parsers remain practical outside a demo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvu61y00hixd60igt8j1f.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvu61y00hixd60igt8j1f.gif" alt="File Viewer v2.2.2 showing multiple file formats entirely in the browser" width="760" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The current English demo routes 206 extensions through 24 lazy browser-side preview pipelines.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Adding file preview to a web application often looks simple at first. Display images with an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element, embed PDFs, and download everything else.&lt;/p&gt;

&lt;p&gt;That approach usually stops working when an internal application accumulates real business attachments: DOCX reports, XLSX workbooks, PPTX decks, legacy Office files, CAD drawings, archives, email messages, diagrams, source code, and structured data. Teams then end up operating a conversion backend, sending files to an external service, or maintaining a collection of unrelated viewers.&lt;/p&gt;

&lt;p&gt;File Viewer is an Apache-2.0 project that treats this problem as frontend infrastructure. It provides a common browser-side loading, rendering, interaction, and deployment layer while keeping format-specific engines modular.&lt;/p&gt;

&lt;p&gt;This article explains the architectural choices behind it, including the tradeoffs and the places where browser-native preview is not the right answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start With Deployment Constraints
&lt;/h2&gt;

&lt;p&gt;The project was designed around internal tools, intranet systems, approval workflows, attachment centers, engineering archives, and private deployments.&lt;/p&gt;

&lt;p&gt;These environments frequently have constraints that are easy to overlook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Documents may not be uploaded to a third-party converter.&lt;/li&gt;
&lt;li&gt;The application may run without public internet access.&lt;/li&gt;
&lt;li&gt;Worker, WASM, font, and vendor assets must come from an approved internal origin.&lt;/li&gt;
&lt;li&gt;A strict Content Security Policy may block dynamically created Workers or remote resources.&lt;/li&gt;
&lt;li&gt;The host application may use Vue, React, Svelte, jQuery, or no framework at all.&lt;/li&gt;
&lt;li&gt;A global design system may contain aggressive CSS resets.&lt;/li&gt;
&lt;li&gt;Most users open common files, but a small number need much heavier CAD or archive engines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those constraints make a single monolithic viewer bundle unattractive. They also make “just run a document conversion server” an incomplete answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate Orchestration From Rendering
&lt;/h2&gt;

&lt;p&gt;The project is divided into several layers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@file-viewer/core&lt;/code&gt; owns the behavior shared by every format:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source loading and format detection;&lt;/li&gt;
&lt;li&gt;renderer registration and selection;&lt;/li&gt;
&lt;li&gt;lifecycle and controller APIs;&lt;/li&gt;
&lt;li&gt;search and highlight state;&lt;/li&gt;
&lt;li&gt;zoom and navigation state;&lt;/li&gt;
&lt;li&gt;print and HTML export coordination;&lt;/li&gt;
&lt;li&gt;download and operation guards;&lt;/li&gt;
&lt;li&gt;theme, watermark, and toolbar configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Format-specific behavior lives in &lt;code&gt;@file-viewer/renderer-*&lt;/code&gt; packages. PDF, Word, spreadsheets, presentations, CAD, archives, email, drawings, geospatial data, and other families can therefore evolve independently.&lt;/p&gt;

&lt;p&gt;This boundary is important. The core should not need to understand how a DWG file is decoded or how a ZIP entry is extracted. It only needs a renderer contract that describes whether the active document supports operations such as search, print, zoom, or export.&lt;/p&gt;

&lt;p&gt;That also prevents the common toolbar from advertising operations a renderer cannot perform reliably. For example, a PDF renderer can provide full-page printing, while an archive browser may expose nested preview and extraction but intentionally hide print.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assemble Capabilities With Presets
&lt;/h2&gt;

&lt;p&gt;Most applications do not need every supported format. File Viewer groups common renderer sets into presets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;@file-viewer/preset-lite&lt;/code&gt; covers text, Markdown, code, images, audio, and video.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@file-viewer/preset-office&lt;/code&gt; covers PDF, Word, Excel, PowerPoint, OFD, RTF, and OpenDocument workflows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@file-viewer/preset-engineering&lt;/code&gt; covers CAD, 3D, geospatial data, diagrams, archives, and engineering formats.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@file-viewer/preset-all&lt;/code&gt; provides the capability set used by the complete demo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An application can also install one renderer directly when it only needs an exact format boundary.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;*-full&lt;/code&gt; framework packages favor fast setup by combining a component package with the full preset. They are useful for evaluation, attachment workbenches, and applications where broad coverage matters more than the smallest possible dependency graph.&lt;/p&gt;

&lt;p&gt;Production applications with a narrower format set should usually start with a native component package and add only the required preset or renderers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lazy Loading Is More Important Than the Extension Count
&lt;/h2&gt;

&lt;p&gt;At the time of writing, the registry declares 206 extension mappings across 24 preview pipelines. That does not mean all 206 implementations are downloaded when the page starts.&lt;/p&gt;

&lt;p&gt;Heavy Office, PDF, CAD, archive, Typst, drawing, PSD, Worker, and WASM paths load when their file types are selected. A user opening Markdown should not first download a CAD decoder.&lt;/p&gt;

&lt;p&gt;The CDN full entry follows the same idea. Its initial script installs the Custom Element, controller, and lazy capability registry. Renderer bundles are fetched from the distribution directory when needed. Worker, WASM, font, and vendor assets can be served from the same origin or copied to a private asset path.&lt;/p&gt;

&lt;p&gt;For Vite applications, an optional plugin can discover installed presets and copy their runtime assets. Other build systems can pass presets explicitly and copy assets as part of their normal deployment process.&lt;/p&gt;

&lt;p&gt;This architecture does not make heavy document engines small. It limits when they are transferred and lets the application exclude engines it never uses. Bundle-size claims should therefore be based on the selected preset and measured production build, not on one universal number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep Framework Integrations Thin
&lt;/h2&gt;

&lt;p&gt;The same core is exposed through packages for Web Components, Vanilla JavaScript, Vue 3, Vue 2.7/2.6, React, legacy React, Svelte, and jQuery.&lt;/p&gt;

&lt;p&gt;Framework packages translate native concepts—props, events, refs, hooks, actions, or plugins—into the same viewer options and controller semantics. They do not maintain separate rendering implementations.&lt;/p&gt;

&lt;p&gt;A basic React integration using the full package looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FileViewer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@file-viewer/react-full&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Preview&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FileViewer&lt;/span&gt;
      &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/files/report.pdf"&lt;/span&gt;
      &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A no-build page can use the Web Component entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://cdn.jsdelivr.net/npm/@file-viewer/web-full@latest/dist/flyfish-file-viewer-web-full.iife.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;flyfish-file-viewer&lt;/span&gt;
  &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/files/report.docx"&lt;/span&gt;
  &lt;span class="na"&gt;theme=&lt;/span&gt;&lt;span class="s"&gt;"light"&lt;/span&gt;
  &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"display:block;height:720px"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/flyfish-file-viewer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Web Component and IIFE paths use Shadow DOM isolation by default. Framework integrations retain light-DOM compatibility but can opt into stronger isolation. CSS variables and stable Shadow Parts provide safer customization than depending on internal class names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be Precise About Privacy
&lt;/h2&gt;

&lt;p&gt;“Browser-side” should not be translated into “the application never makes a network request.”&lt;/p&gt;

&lt;p&gt;File Viewer does not require a server-side document conversion service. A local file can be parsed and rendered in the browser, and runtime assets can be self-hosted. This means a private document does not need to be sent to a third-party SaaS converter.&lt;/p&gt;

&lt;p&gt;The final privacy boundary still depends on deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A remote document URL must be fetched from somewhere.&lt;/li&gt;
&lt;li&gt;Worker, WASM, fonts, and vendor scripts use the origins configured by the application.&lt;/li&gt;
&lt;li&gt;Optional integrations, such as a configured PlantUML rendering endpoint, can introduce another request.&lt;/li&gt;
&lt;li&gt;Application telemetry remains the host application’s responsibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For an offline or intranet deployment, teams should self-host the complete runtime asset set, audit configured endpoints, and test under the production CSP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extension Support Is Not a Fidelity Guarantee
&lt;/h2&gt;

&lt;p&gt;A long extension list can become misleading if every recognized file is described as fully rendered.&lt;/p&gt;

&lt;p&gt;The project documents several capability levels. Mature pipelines provide visual preview and useful document operations. Some specialized engineering formats currently provide safe structure inspection, metadata, or conversion guidance. Extremely complex formats may require a dedicated C++, Rust, or WASM engine before high-fidelity rendering is realistic.&lt;/p&gt;

&lt;p&gt;Office preview also has a different goal from running Microsoft Office itself. Complex pagination, fonts, SmartArt, embedded objects, macros, and vendor-specific layout behavior can produce differences. The correct evaluation method is to test representative, sanitized production files and review the published fidelity notes.&lt;/p&gt;

&lt;p&gt;The component is also read-only. Download preserves the original bytes; it does not write rendered changes back into the source document. Applications needing collaborative editing, tracked changes, or Office-compatible authoring should evaluate a document editing suite instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Self-Hosting and Evaluation
&lt;/h2&gt;

&lt;p&gt;The project provides npm packages, static release artifacts, and a Docker image. A local demo can be started with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 flyfishdev/file-viewer:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For evaluation, start with the live samples, then test sanitized files that resemble production data. Verify rendering, search, print, asset loading, mobile behavior, and CSP rules before selecting a preset.&lt;/p&gt;

&lt;p&gt;Browser-native preview is a good fit when an application needs read-only access to mixed attachments, private deployment, frontend integration, and a consistent interaction layer. It is a weaker fit when exact Office editing, server-generated canonical output, or professional CAD authoring is the main requirement.&lt;/p&gt;

&lt;p&gt;The useful question is not whether one viewer can replace every native application. It is whether a modular browser preview layer can remove enough downloads, conversion jobs, and one-off integrations to improve a real workflow.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/flyfish-dev/file-viewer" rel="noopener noreferrer"&gt;https://github.com/flyfish-dev/file-viewer&lt;/a&gt;&lt;br&gt;&lt;br&gt;
English demo: &lt;a href="https://demo.file-viewer.app/?lang=en-US" rel="noopener noreferrer"&gt;https://demo.file-viewer.app/?lang=en-US&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Documentation: &lt;a href="https://doc.file-viewer.app/en/" rel="noopener noreferrer"&gt;https://doc.file-viewer.app/en/&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Format notes: &lt;a href="https://doc.file-viewer.app/en/guide/format-fidelity" rel="noopener noreferrer"&gt;https://doc.file-viewer.app/en/guide/format-fidelity&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
