<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://haku-d.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://haku-d.github.io/" rel="alternate" type="text/html" /><updated>2026-11-03T20:44:51+00:00</updated><id>https://haku-d.github.io/feed.xml</id><title type="html">Canadalogy</title><subtitle>Like to share what I have learned and what I am doing</subtitle><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><entry><title type="html">PDE — Packaging Dieline Editor with 2D/3D Live Preview</title><link href="https://haku-d.github.io/nextjs/typescript/threejs/react/2d/3d/pde-packaging-dieline-editor/" rel="alternate" type="text/html" title="PDE — Packaging Dieline Editor with 2D/3D Live Preview" /><published>2026-08-12T16:00:00+00:00</published><updated>2026-08-12T16:00:00+00:00</updated><id>https://haku-d.github.io/nextjs/typescript/threejs/react/2d/3d/pde-packaging-dieline-editor</id><content type="html" xml:base="https://haku-d.github.io/nextjs/typescript/threejs/react/2d/3d/pde-packaging-dieline-editor/"><![CDATA[<h2 id="background">Background</h2>

<p>Packaging design for折叠 cartons (folding cartons) involves working with <em>dielines</em> — flat layouts that show where cuts, folds, and glue areas will be before the box is assembled. Traditionally this is done in Illustrator or specialized CAD software. I wanted to see if I could build a web-based tool that lets designers edit dielines visually and see the finished 3D box fold in real time.</p>

<p>The project is a live proof of concept: <a href="https://github.com/haku-d/pde">github.com/haku-d/pde</a></p>

<blockquote>
  <p><strong>Note:</strong> All source code in this project was AI-generated by different models. I’m treating this as an experiment — I’ll share what worked, what didn’t, and what I’d do differently.</p>
</blockquote>

<h2 id="what-pde-does">What PDE does</h2>

<p>PDE has two editor surfaces:</p>

<h3 id="1-parametric-dieline-editor">1. Parametric Dieline Editor</h3>

<p>The default route. Feed it dimensions and closure parameters — length, width, height, flap types, tuck flap geometry, dust flaps, glue flaps, material thickness, fold allowance, bleed, safe area — and it generates a complete card-box dieline from the <code class="language-plaintext highlighter-rouge">card-box-basic</code> model.</p>

<p>From a single set of parameters you get:</p>
<ul>
  <li>Stable panel IDs, cut lines, fold lines, bleed paths, safe-area paths</li>
  <li>Validation issues if the geometry is invalid</li>
  <li>A compiled shape that the shared renderer can preview in both 2D and 3D</li>
</ul>

<p>Editor controls include unit switching (<code class="language-plaintext highlighter-rouge">mm</code>, <code class="language-plaintext highlighter-rouge">cm</code>, <code class="language-plaintext highlighter-rouge">in</code>), debounced live regeneration as you edit fields, per-guide visibility and color toggles, and a JSON or SVG export.</p>

<h3 id="2-dieline-to-3d-editor">2. Dieline-to-3D Editor</h3>

<p>Loads fixed template JSON files or imports annotated SVGs, then renders the same 2D canvas texture onto a foldable 3D preview. The SVG importer recognizes common dieline line classes — cut, fold/crease, bleed, safe-area, dimension, construction, glue-area — from the SVG metadata.</p>

<h2 id="the-2d3d-renderer-pipeline">The 2D/3D renderer pipeline</h2>

<p>Both editors share a renderer pipeline I built as part of this project:</p>

<ul>
  <li><strong>Canvas-based 2D rendering</strong> — renders trim, crease, bleed, safe-area, dimensions, panels, and demo artwork onto a <code class="language-plaintext highlighter-rouge">&lt;canvas&gt;</code></li>
  <li><strong>Texture canvas generation</strong> — builds the material texture for 3D panels from the same 2D data</li>
  <li><strong>Three.js panel meshes</strong> — UV-mapped panels with material thickness</li>
  <li><strong>Fold graph construction</strong> — builds the fold order graph so the 3D animation knows which panel folds into which</li>
  <li><strong>Fold animation</strong> — open/close controls with a fold progress slider</li>
  <li><strong>Viewport</strong> — pan, zoom, fit, and keyboard navigation in both 2D and 3D views</li>
</ul>

<h2 id="project-structure">Project structure</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre>src/app/                  Next.js App Router pages and template API
src/components/           Shared editor layout and canvas UI
src/editors/             Client-side editor orchestration
src/core/models/         Parametric model registry and field definitions
src/core/parameters/     Parameter and dimension helpers
src/core/structure/      Fold graph and structural validation
src/core/template/       Template compiler, schema, serializers, SVG importer
src/core/units/          Unit conversion utilities
src/render2d/           Canvas renderer, hit testing, viewport, SVG export
src/render3d/            Three.js scene builder, preview, fold animation
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="running-it-locally">Running it locally</h2>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre>git clone https://github.com/haku-d/pde.git
<span class="nb">cd </span>pde
npm <span class="nb">install
</span>npm run dev
<span class="c"># → http://localhost:3000 redirects to /editors/parametric-dieline</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Available routes:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">/</code> → redirects to parametric editor</li>
  <li><code class="language-plaintext highlighter-rouge">/editors/parametric-dieline</code> → parametric folding carton editor</li>
  <li><code class="language-plaintext highlighter-rouge">/editors/dieline-to-3d</code> → fixed-template and SVG-import editor</li>
  <li><code class="language-plaintext highlighter-rouge">/api/templates/[templateId]</code> → serves bundled template fixtures</li>
</ul>

<h2 id="what-the-ai-generation-experiment-taught-me">What the AI-generation experiment taught me</h2>

<p>This whole project was generated by AI — different models for different parts. Here are a few honest observations:</p>

<p><strong>What worked well:</strong></p>
<ul>
  <li>Boilerplate-heavy modules (parameter engines, unit converters, test suites) were generated quickly and mostly correctly</li>
  <li>The Three.js scene/fold animation was cleaner than I expected given how much boilerplate it requires</li>
  <li>Test scaffolding for core geometry was solid enough to build on</li>
</ul>

<p><strong>What needed heavy manual correction:</strong></p>
<ul>
  <li>SVG import heuristics — the “recognize common dieline line classes” logic required significant manual tuning</li>
  <li>The fold graph construction had edge cases around multi-panel folds that required rethinking the data model</li>
  <li>The parametric model’s validation logic had boundary condition bugs that only surfaced when testing with real-world dimension ranges</li>
</ul>

<p><strong>What I’d do differently:</strong></p>
<ul>
  <li>Start with the data model for the fold graph before any rendering code — it’s the foundation everything else builds on</li>
  <li>Treat the 2D canvas renderer and the texture generator as one module, not two — keeping them in sync manually is error-prone</li>
  <li>The 3D panel mesh UV mapping was the hardest part — I’d invest in a dedicated UV-atlas step instead of trying to share canvas coordinates directly</li>
</ul>

<h2 id="current-state-and-next-steps">Current state and next steps</h2>

<p>PDE is an active proof of concept, not production software. The current limitations:</p>

<ul>
  <li>Only one registered parametric model: <code class="language-plaintext highlighter-rouge">card-box-basic</code></li>
  <li>The 3D preview is an MVP renderer, not a manufacturing simulator</li>
  <li>SVG import is deterministic for controlled/annotated SVGs and best-effort for arbitrary SVGs</li>
  <li>Production-grade CAD operations, artwork persistence, and multi-format export are still future work</li>
</ul>

<p>The immediate next piece I’m working on is improving the hinges surface in the 3D preview — getting the fold animation to feel more physically accurate rather than just geometrically correct.</p>

<h2 id="source">Source</h2>

<p><a href="https://github.com/haku-d/pde">github.com/haku-d/pde</a></p>]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="nextjs" /><category term="typescript" /><category term="threejs" /><category term="react" /><category term="2d" /><category term="3d" /><summary type="html"><![CDATA[A Next.js proof of concept for editing packaging dielines in 2D and previewing live foldable 3D models — all source code AI-generated as an experiment.]]></summary></entry><entry><title type="html">Self-hosted prerender service for SPA SEO</title><link href="https://haku-d.github.io/node/typescript/seo/puppeteer/fastify/nginx/docker/self-hosted-prerender-service-for-spa-seo/" rel="alternate" type="text/html" title="Self-hosted prerender service for SPA SEO" /><published>2026-07-14T16:24:38+00:00</published><updated>2026-07-14T16:24:38+00:00</updated><id>https://haku-d.github.io/node/typescript/seo/puppeteer/fastify/nginx/docker/self-hosted-prerender-service-for-spa-seo</id><content type="html" xml:base="https://haku-d.github.io/node/typescript/seo/puppeteer/fastify/nginx/docker/self-hosted-prerender-service-for-spa-seo/"><![CDATA[<p>Single-page applications are great for building rich user experiences, but they can still be challenging for SEO bots, link previews, and crawlers. Some crawlers execute JavaScript well, some do not, and social preview bots usually expect useful HTML immediately.</p>

<p>I created a self-hosted prerender service to solve this problem for JavaScript-heavy SPAs. The project is available on GitHub: <a href="https://github.com/haku-d/prerender">https://github.com/haku-d/prerender</a></p>

<h2 id="what-this-project-does">What this project does</h2>

<p>The service detects bot traffic and serves pre-rendered HTML instead of the normal JavaScript application shell. Regular users still receive the SPA as usual, while crawlers receive a fully rendered HTML snapshot.</p>

<p>Key features:</p>

<ul>
  <li>Detects SEO bots and social crawlers from <code class="language-plaintext highlighter-rouge">User-Agent</code></li>
  <li>Uses <code class="language-plaintext highlighter-rouge">Nginx</code> to route bot requests to the prerender service</li>
  <li>Renders pages with <code class="language-plaintext highlighter-rouge">Puppeteer Core</code> and headless Chromium</li>
  <li>Stores rendered HTML in a disk cache</li>
  <li>Uses <code class="language-plaintext highlighter-rouge">CACHE_TTL_SECONDS</code> to refresh stale pages</li>
  <li>Warms the cache by crawling the site’s XML sitemap</li>
  <li>Supports <code class="language-plaintext highlighter-rouge">410 Gone</code> markers for removed pages</li>
  <li>Supports <code class="language-plaintext highlighter-rouge">301 Redirect</code> mappings for old URLs</li>
  <li>Stores bot analytics in <code class="language-plaintext highlighter-rouge">PostgreSQL</code></li>
  <li>Exposes an analytics API for bot traffic and cache status</li>
</ul>

<h2 id="how-it-works">How it works</h2>

<p>At a high level, the architecture is simple:</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">Nginx</code> receives every request.</li>
  <li>Static assets such as JavaScript, CSS, images, and fonts are served normally.</li>
  <li>For regular users, <code class="language-plaintext highlighter-rouge">Nginx</code> proxies the request to the SPA origin.</li>
  <li>For known bots, <code class="language-plaintext highlighter-rouge">Nginx</code> forwards the request to the prerender service.</li>
  <li>The prerender service checks whether a cached HTML file already exists.</li>
  <li>If the cache is fresh, it returns the cached HTML immediately.</li>
  <li>If the cache is missing or stale, Puppeteer opens the page, waits for hydration, extracts the HTML, minifies it, saves it to disk, and returns it.</li>
  <li>Bot request details are optionally stored in PostgreSQL for analytics.</li>
</ol>

<p>The important part is that regular users are not affected. Only bots and crawlers go through the rendering pipeline.</p>

<h2 id="why-self-host-it">Why self-host it?</h2>

<p>There are existing hosted prerender services, but I wanted more control over the full pipeline:</p>

<ul>
  <li>Control the bot detection rules</li>
  <li>Keep rendered HTML cache on my own infrastructure</li>
  <li>Avoid sending crawler traffic through a third-party SaaS service</li>
  <li>Add custom cache invalidation behavior</li>
  <li>Add analytics around bot traffic, cache hit rate, and render duration</li>
  <li>Integrate it with my own deployment and Nginx setup</li>
</ul>

<p>For small and medium applications, this approach can be cost-effective because most bot requests are served from disk cache after the first render.</p>

<h2 id="tech-stack">Tech stack</h2>

<table>
  <thead>
    <tr>
      <th>Layer</th>
      <th>Technology</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Runtime</td>
      <td>Node.js 20, TypeScript</td>
    </tr>
    <tr>
      <td>HTTP framework</td>
      <td>Fastify 5</td>
    </tr>
    <tr>
      <td>Browser rendering</td>
      <td>Puppeteer Core + Chromium</td>
    </tr>
    <tr>
      <td>Cache storage</td>
      <td>Disk cache</td>
    </tr>
    <tr>
      <td>Database</td>
      <td>PostgreSQL 16</td>
    </tr>
    <tr>
      <td>Migration tool</td>
      <td>dbmate</td>
    </tr>
    <tr>
      <td>Scheduler</td>
      <td>toad-scheduler</td>
    </tr>
    <tr>
      <td>HTML optimization</td>
      <td>html-minifier-terser</td>
    </tr>
    <tr>
      <td>Reverse proxy</td>
      <td>Nginx</td>
    </tr>
    <tr>
      <td>Deployment</td>
      <td>Docker Compose</td>
    </tr>
  </tbody>
</table>

<h2 id="docker-services">Docker services</h2>

<p>The repository includes a <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> with three services:</p>

<table>
  <thead>
    <tr>
      <th>Service</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">prerender</code></td>
      <td>Main Fastify service running Puppeteer and cache logic</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">postgres</code></td>
      <td>Optional analytics database</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">prerender-ui</code></td>
      <td>Optional dashboard for analytics and management</td>
    </tr>
  </tbody>
</table>

<p>The minimum environment configuration looks like this:</p>

<pre><code class="language-env">PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
SITE_URL=https://your-site.com
STATIC_SITE_DIR=/data/prerender
DATABASE_URL=postgresql://prerender:secret@postgres:5432/prerender
POSTGRES_PASSWORD=secret
CACHE_TTL_SECONDS=43200
EXECUTE_JOB_ON_START=false
</code></pre>

<p>Then start the service:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker compose up <span class="nt">-d</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>If analytics are not required, <code class="language-plaintext highlighter-rouge">postgres</code>, <code class="language-plaintext highlighter-rouge">DATABASE_URL</code>, and <code class="language-plaintext highlighter-rouge">prerender-ui</code> can be removed.</p>

<h2 id="nginx-bot-detection">Nginx bot detection</h2>

<p>The key part is the <code class="language-plaintext highlighter-rouge">Nginx</code> configuration. It maps bot user agents to a prerender flag:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="k">map</span> <span class="nv">$http_user_agent</span> <span class="nv">$prerender_ua</span> <span class="p">{</span>
    <span class="kn">default</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kn">"~*googlebot"</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kn">"~*bingbot"</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kn">"~*facebookexternalhit"</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kn">"~*twitterbot"</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kn">"~*linkedinbot"</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kn">"~*slackbot"</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kn">"~*discordbot"</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Static assets are excluded so the service only renders real pages:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="k">map</span> <span class="nv">$uri</span> <span class="nv">$prerender</span> <span class="p">{</span>
    <span class="kn">default</span> <span class="nv">$x_prerender</span><span class="p">;</span>
    <span class="kn">"~*\.</span><span class="s">(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)"</span> <span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>When <code class="language-plaintext highlighter-rouge">$prerender</code> is enabled, the request is rewritten to the prerender backend:</p>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre></td><td class="rouge-code"><pre><span class="k">location</span> <span class="n">/</span> <span class="p">{</span>
    <span class="kn">if</span> <span class="s">(</span><span class="nv">$prerender</span> <span class="p">=</span> <span class="mi">1</span><span class="s">)</span> <span class="p">{</span>
        <span class="kn">rewrite</span> <span class="s">(.*)</span> <span class="n">/prerender-proxy</span> <span class="s">last</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kn">proxy_pass</span> <span class="s">http://s3_bucket</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">location</span> <span class="n">/prerender-proxy</span> <span class="p">{</span>
    <span class="kn">rewrite</span> <span class="s">.*</span> <span class="n">/https://</span><span class="nv">$host$request_uri</span><span class="s">?</span> <span class="s">break</span><span class="p">;</span>
    <span class="kn">proxy_set_header</span> <span class="s">X-Prerender</span> <span class="mi">1</span><span class="p">;</span>
    <span class="kn">proxy_pass</span> <span class="s">http://prerender_backend</span><span class="p">;</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">X-Prerender</code> header is important because it prevents a request from being routed back into the prerender flow again.</p>

<h2 id="cache-behavior">Cache behavior</h2>

<p>Rendered pages are stored as HTML files under <code class="language-plaintext highlighter-rouge">STATIC_SITE_DIR</code>. Each cache file is keyed by an MD5 hash of the full canonical URL.</p>

<p>The service supports these cache states:</p>

<table>
  <thead>
    <tr>
      <th>Status</th>
      <th>Meaning</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">hit</code></td>
      <td>Fresh cached HTML was returned</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">miss</code></td>
      <td>No cached file existed, so Puppeteer rendered the page</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">stale</code></td>
      <td>Cached HTML existed but passed the TTL and needed refresh</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gone</code></td>
      <td>URL was marked as permanently removed</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">redirect</code></td>
      <td>URL matched a stored redirect mapping</td>
    </tr>
  </tbody>
</table>

<p>The default TTL is <code class="language-plaintext highlighter-rouge">43200</code> seconds, or 12 hours.</p>

<h2 id="useful-endpoints">Useful endpoints</h2>

<p>The service is intended to run privately behind <code class="language-plaintext highlighter-rouge">Nginx</code>, so the API is intentionally simple.</p>

<h3 id="force-refresh-one-page">Force refresh one page</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="err">GET /hit/https://example.com/products/business-cards
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>This re-renders the page and overwrites the cache entry.</p>

<h3 id="mark-a-page-as-gone">Mark a page as gone</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="err">GET /gone/https://example.com/old-page
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>Future bot requests for that URL return <code class="language-plaintext highlighter-rouge">410 Gone</code>.</p>

<h3 id="add-a-redirect">Add a redirect</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="err">GET /redirect/https://example.com/old-path/https://example.com/new-path
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>Future bot requests for the old URL return a <code class="language-plaintext highlighter-rouge">301</code> redirect.</p>

<h3 id="view-analytics-summary">View analytics summary</h3>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="err">GET /analytics/summary?from=2026-07-01&amp;to=2026-07-14
</span></pre></td></tr></tbody></table></code></pre></div></div>

<p>Example response:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="p">{</span><span class="w">
  </span><span class="nl">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">1240</span><span class="p">,</span><span class="w">
  </span><span class="nl">"uniquePages"</span><span class="p">:</span><span class="w"> </span><span class="mi">87</span><span class="p">,</span><span class="w">
  </span><span class="nl">"byBot"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"botName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"googlebot"</span><span class="p">,</span><span class="w"> </span><span class="nl">"count"</span><span class="p">:</span><span class="w"> </span><span class="mi">640</span><span class="w"> </span><span class="p">}],</span><span class="w">
  </span><span class="nl">"byCacheStatus"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="w"> </span><span class="nl">"cacheStatus"</span><span class="p">:</span><span class="w"> </span><span class="s2">"hit"</span><span class="p">,</span><span class="w"> </span><span class="nl">"count"</span><span class="p">:</span><span class="w"> </span><span class="mi">980</span><span class="w"> </span><span class="p">}]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="what-i-learned">What I learned</h2>

<p>The biggest lesson is that prerendering is not only about running Puppeteer. The browser rendering part is important, but the real value comes from the surrounding system:</p>

<ul>
  <li>Good bot detection</li>
  <li>Avoiding static asset rendering</li>
  <li>Preventing proxy loops</li>
  <li>Cache freshness and invalidation</li>
  <li>Sitemap-based cache warming</li>
  <li>Handling deleted and redirected pages correctly</li>
  <li>Measuring cache hit rate and render performance</li>
</ul>

<p>A prerender service becomes much more useful when it behaves like infrastructure, not just a script that opens pages in Chromium.</p>

<h2 id="source-code">Source code</h2>

<p>You can find the full project here:</p>

<p><a href="https://github.com/haku-d/prerender">https://github.com/haku-d/prerender</a></p>]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="node" /><category term="typescript" /><category term="seo" /><category term="puppeteer" /><category term="fastify" /><category term="nginx" /><category term="docker" /><summary type="html"><![CDATA[Single-page applications are great for building rich user experiences, but they can still be challenging for SEO bots, link previews, and crawlers. Some crawlers execute JavaScript well, some do not, and social preview bots usually expect useful HTML immediately.]]></summary></entry><entry><title type="html">Create data grid in angular with primeng</title><link href="https://haku-d.github.io/angular/primeng/editable/table/datagrid/grid/create-editable-table-with-primeng/" rel="alternate" type="text/html" title="Create data grid in angular with primeng" /><published>2024-10-29T13:14:38+00:00</published><updated>2024-10-29T13:14:38+00:00</updated><id>https://haku-d.github.io/angular/primeng/editable/table/datagrid/grid/create-editable-table-with-primeng</id><content type="html" xml:base="https://haku-d.github.io/angular/primeng/editable/table/datagrid/grid/create-editable-table-with-primeng/"><![CDATA[<h3 id="define-column-types-and-cell-types">Define Column Types and Cell Types</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="kr">enum</span> <span class="nx">TableCellType</span> <span class="p">{</span>
  <span class="nx">Input</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">input</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">InputNumber</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">inputNumber</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">Checkbox</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">checkbox</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">Dropdown</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">dropdown</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">Icon</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">icon</span><span class="dl">'</span><span class="p">,</span>
  <span class="nb">Date</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">date</span><span class="dl">'</span><span class="p">,</span>
  <span class="nx">Link</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">link</span><span class="dl">'</span><span class="p">,</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="kd">type</span> <span class="nx">TableColumn</span> <span class="o">=</span> <span class="p">{</span>
  <span class="na">header</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span><span class="nx">header</span><span class="p">.</span>
  <span class="na">field</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">cellType</span><span class="p">?:</span> <span class="nx">TableCellType</span><span class="p">;</span>
  <span class="nl">width</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">editable</span><span class="p">?:</span> <span class="nx">boolean</span><span class="p">;</span>
  <span class="nl">align</span><span class="p">?:</span> <span class="dl">'</span><span class="s1">left</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">right</span><span class="dl">'</span> <span class="o">|</span> <span class="dl">'</span><span class="s1">center</span><span class="dl">'</span><span class="p">;</span>
  <span class="nl">iconClass</span><span class="p">?:</span> <span class="kr">string</span><span class="p">;</span>
  <span class="nl">iconOnClick</span><span class="p">?:</span> <span class="nx">AnyFunc</span><span class="p">;</span>
<span class="p">};</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<ul>
  <li>header: <code class="language-plaintext highlighter-rouge">string</code>: Represents the column’s display name in the table header.</li>
  <li>field: <code class="language-plaintext highlighter-rouge">string</code>: Specifies the key in the data source that the column will use.</li>
</ul>

<h3 id="define-columns">Define Columns</h3>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nx">cols</span><span class="p">:</span> <span class="nx">TableColumn</span><span class="p">[]</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span>
      <span class="na">header</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Code</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">field</span><span class="p">:</span> <span class="dl">'</span><span class="s1">code</span><span class="dl">'</span><span class="p">,</span>
      <span class="na">editable</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
      <span class="na">cellType</span><span class="p">:</span> <span class="nx">TableCellType</span><span class="p">.</span><span class="nx">Input</span><span class="p">,</span>
      <span class="na">width</span><span class="p">:</span> <span class="dl">'</span><span class="s1">100px</span><span class="dl">'</span><span class="p">,</span>
    <span class="p">},</span>
    <span class="c1">// More column definitions...</span>
  <span class="p">];</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="define-form-group">Define Form Group</h3>

<p>To handle the data updates, I use a FormGroup that keeps track of each row’s data:</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="nx">formGroup</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FormGroup</span><span class="p">({</span>
  <span class="na">id</span><span class="p">:</span> <span class="k">new</span> <span class="nx">FormControl</span><span class="p">(),</span>
  <span class="na">code</span><span class="p">:</span> <span class="k">new</span> <span class="nx">FormControl</span><span class="p">(),</span>
  <span class="na">name</span><span class="p">:</span> <span class="k">new</span> <span class="nx">FormControl</span><span class="p">(),</span>
  <span class="na">active</span><span class="p">:</span> <span class="k">new</span> <span class="nx">FormControl</span><span class="p">(),</span>
  <span class="na">price</span><span class="p">:</span> <span class="k">new</span> <span class="nx">FormControl</span><span class="p">(),</span>
  <span class="na">category</span><span class="p">:</span> <span class="k">new</span> <span class="nx">FormControl</span><span class="p">(),</span>
  <span class="na">quantity</span><span class="p">:</span> <span class="k">new</span> <span class="nx">FormControl</span><span class="p">(),</span>
<span class="p">});</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Since only one row can be edited at a time, the row’s data is updated into the form group during editing. To listen for editing events, I use <code class="language-plaintext highlighter-rouge">onEditInit</code> and <code class="language-plaintext highlighter-rouge">onEditComplete</code></p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="nx">onEditInit</span><span class="p">(</span><span class="nx">event</span><span class="p">:</span> <span class="nx">TableEditInitEvent</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">entity</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">products</span><span class="p">[</span><span class="nx">event</span><span class="p">.</span><span class="nx">index</span><span class="p">];</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">formGroup</span><span class="p">.</span><span class="nx">setValue</span><span class="p">(</span>
    <span class="nx">pick</span><span class="p">(</span><span class="nx">entity</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">id</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">code</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">name</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">active</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">price</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">category</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">quantity</span><span class="dl">'</span><span class="p">]),</span>
    <span class="p">{</span> <span class="na">emitEvent</span><span class="p">:</span> <span class="kc">false</span> <span class="p">}</span>
  <span class="p">);</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nx">onEditComplete</span><span class="p">(</span><span class="nx">event</span><span class="p">:</span> <span class="nx">TableEditCompleteEvent</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">formGroup</span><span class="p">.</span><span class="nx">dirty</span> <span class="o">&amp;&amp;</span> <span class="nx">event</span><span class="p">.</span><span class="nx">index</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">products</span> <span class="o">=</span> <span class="p">[</span>
      <span class="p">...</span><span class="k">this</span><span class="p">.</span><span class="nx">products</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">event</span><span class="p">.</span><span class="nx">index</span><span class="p">),</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">formGroup</span><span class="p">.</span><span class="nx">value</span><span class="p">,</span>
      <span class="p">...</span><span class="k">this</span><span class="p">.</span><span class="nx">products</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">index</span> <span class="o">+</span> <span class="mi">1</span><span class="p">),</span>
    <span class="p">];</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">formGroup</span><span class="p">.</span><span class="nx">markAsPristine</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="initialize-primeng-table">Initialize PrimeNG Table</h3>

<p>The table is initialized in the HTML as follows:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
</pre></td><td class="rouge-code"><pre><span class="nt">&lt;p-table</span>
  <span class="na">[value]=</span><span class="s">"products"</span>
  <span class="na">dataKey=</span><span class="s">"id"</span>
  <span class="na">[tableStyle]=</span><span class="s">"{ 'min-width': '50rem' }"</span>
  <span class="na">selectionMode=</span><span class="s">"single"</span>
  <span class="na">[columns]=</span><span class="s">"cols"</span>
  <span class="na">(onEditInit)=</span><span class="s">"onEditInit($event)"</span>
  <span class="na">(onEditComplete)=</span><span class="s">"onEditComplete($event)"</span>
  <span class="nt">&gt;</span>
<span class="nt">&lt;/p-table&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="initialize-the-table-colgroup">Initialize the Table Colgroup</h3>

<p>To control the column widths:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
</pre></td><td class="rouge-code"><pre><span class="nt">&lt;ng-template</span> <span class="na">pTemplate=</span><span class="s">"colgroup"</span> <span class="na">let-columns</span><span class="nt">&gt;</span>
  <span class="nt">&lt;colgroup&gt;</span>
    <span class="nt">&lt;col</span>
      <span class="na">*ngFor=</span><span class="s">"let col of columns"</span>
      <span class="na">span=</span><span class="s">"1"</span>
      <span class="na">[style.width]=</span><span class="s">"col.width ? col.width : 'auto'"</span>
    <span class="nt">/&gt;</span>
  <span class="nt">&lt;/colgroup&gt;</span>
<span class="nt">&lt;/ng-template&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="initialize-the-table-header">Initialize the Table Header</h3>

<p>The header is rendered as follows:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="nt">&lt;ng-template</span> <span class="na">pTemplate=</span><span class="s">"header"</span> <span class="na">let-columns</span><span class="nt">&gt;</span>
  <span class="nt">&lt;tr&gt;</span>
    <span class="nt">&lt;th</span>
      <span class="na">*ngFor=</span><span class="s">"let col of columns"</span>
      <span class="na">[class.text-center]=</span><span class="s">"col.align === 'center'"</span>
      <span class="na">[class.text-right]=</span><span class="s">"col.align === 'right'"</span>
    <span class="nt">&gt;</span>
      {{ col.header }}
    <span class="nt">&lt;/th&gt;</span>
  <span class="nt">&lt;/tr&gt;</span>
<span class="nt">&lt;/ng-template&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="initialize-the-table-body">Initialize the Table Body</h3>

<p>For rendering the table body dynamically and handling inline editing:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
</pre></td><td class="rouge-code"><pre><span class="nt">&lt;ng-template</span>
  <span class="na">pTemplate=</span><span class="s">"body"</span>
  <span class="na">let-rowData</span>
  <span class="na">let-columns=</span><span class="s">"columns"</span>
  <span class="na">let-i=</span><span class="s">"rowIndex"</span>
<span class="nt">&gt;</span>
  <span class="nt">&lt;tr&gt;</span>
    <span class="nt">&lt;td</span>
      <span class="na">*ngFor=</span><span class="s">"let col of columns"</span>
      <span class="na">[class.text-center]=</span><span class="s">"col.align === 'center'"</span>
      <span class="na">[class.text-right]=</span><span class="s">"col.align === 'right'"</span>
      <span class="na">[pEditableColumn]=</span><span class="s">"rowData[col.field]"</span>
      <span class="na">[pEditableColumnField]=</span><span class="s">"col.field"</span>
      <span class="na">[pEditableColumnRowIndex]=</span><span class="s">"i"</span>
      <span class="na">[pEditableColumnDisabled]=</span><span class="s">"!col.editable"</span>
    <span class="nt">&gt;</span>
      <span class="nt">&lt;p-cellEditor&gt;</span>
        <span class="nt">&lt;ng-template</span> <span class="na">pTemplate=</span><span class="s">"input"</span><span class="nt">&gt;</span>
          @switch (col.cellType) {
            @case (cellType.InputNumber) {
              <span class="nt">&lt;form</span> <span class="na">[formGroup]=</span><span class="s">"formGroup"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;p-inputNumber</span> <span class="na">[formControlName]=</span><span class="s">"col.field"</span> <span class="na">inputStyleClass=</span><span class="s">"w-full text-right"</span> <span class="nt">/&gt;</span>
              <span class="nt">&lt;/form&gt;</span>
            }
            @default {
              <span class="nt">&lt;form</span> <span class="na">[formGroup]=</span><span class="s">"formGroup"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;input</span> <span class="na">[formControlName]=</span><span class="s">"col.field"</span> <span class="na">pInputText</span> <span class="na">class=</span><span class="s">"w-full"</span> <span class="nt">/&gt;</span>
              <span class="nt">&lt;/form&gt;</span>
            }
          }
        <span class="nt">&lt;/ng-template&gt;</span>
        <span class="nt">&lt;ng-template</span> <span class="na">pTemplate=</span><span class="s">"output"</span><span class="nt">&gt;</span>
          @switch (col.cellType) {
            @case (cellType.Dropdown) {
              <span class="nt">&lt;p-dropdown</span> 
                <span class="na">[options]=</span><span class="s">"categories"</span> 
                <span class="na">optionLabel=</span><span class="s">"label"</span>
                <span class="na">optionValue=</span><span class="s">"label"</span>
                <span class="na">appendTo=</span><span class="s">"body"</span>
                <span class="na">[ngModel]=</span><span class="s">"rowData[col.field]"</span>
                <span class="na">placeholder=</span><span class="s">"Select a category"</span> <span class="nt">/&gt;</span>
            }
            @case (cellType.Checkbox) {
              <span class="nt">&lt;p-checkbox</span> <span class="na">[binary]=</span><span class="s">"true"</span>  <span class="na">[ngModel]=</span><span class="s">"rowData[col.field]"</span><span class="nt">/&gt;</span>
            }
            @case (cellType.Icon) {
              <span class="nt">&lt;i</span>
                <span class="na">class=</span><span class="s">"{{ col.iconClass }}"</span>
                <span class="na">[ngStyle]=</span><span class="s">"{ cursor: 'pointer' }"</span>
                <span class="na">(click)=</span><span class="s">"
                  col.iconOnClick &amp;&amp;
                    col.iconOnClick(rowData[col.field], i, $event)
                "</span>
              <span class="nt">&gt;&lt;/i&gt;</span>
            }
            @default  {
              {{ rowData[col.field] }}
            }
          }
        <span class="nt">&lt;/ng-template&gt;</span>
      <span class="nt">&lt;/p-cellEditor&gt;</span>
    <span class="nt">&lt;/td&gt;</span>
  <span class="nt">&lt;/tr&gt;</span>
<span class="nt">&lt;/ng-template&gt;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The complete source code can be found on <a href="https://stackblitz.com/~/github.com/haku-d/ng-editable-table">Stackblitz</a>.</p>

<h2 id="summary">Summary</h2>

<p>You may have noticed that the left and right arrow keys trigger a focus event on the table, which should ideally be disabled while editing a cell. To achieve this, you can create a custom directive and apply it to each cell to override this behavior.</p>

<p>Additionally, you can enhance the grid by adding a feature that selects the cell’s data automatically when it receives focus, making editing more convenient.</p>

<p>Just drop me a message if you have any question.</p>

<p>Thanks for reading!</p>

<hr />]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="angular" /><category term="primeng" /><category term="editable" /><category term="table" /><category term="datagrid" /><category term="grid" /><summary type="html"><![CDATA[Define Column Types and Cell Types]]></summary></entry><entry><title type="html">Deploy angular monorepo to AWS S3 by github workflow</title><link href="https://haku-d.github.io/angular/aws/github/s3/monorepo/build-and-deploy-angular-to-aws-s3-with-github-action/" rel="alternate" type="text/html" title="Deploy angular monorepo to AWS S3 by github workflow" /><published>2024-10-12T13:14:38+00:00</published><updated>2024-10-12T13:14:38+00:00</updated><id>https://haku-d.github.io/angular/aws/github/s3/monorepo/build-and-deploy-angular-to-aws-s3-with-github-action</id><content type="html" xml:base="https://haku-d.github.io/angular/aws/github/s3/monorepo/build-and-deploy-angular-to-aws-s3-with-github-action/"><![CDATA[<p>The guideline is created for <code class="language-plaintext highlighter-rouge">Nx monorepo</code> workspace only to build and deploy affected apps to AWS S3 by github workflow.</p>

<p>Let’s consider the below monorepo structure
.
└── YourMonoRepo/
    ├── apps/
    │   ├── app1
    │   └── app2
    └── libs/
        ├── lib1
        └── lib2</p>

<h3 id="the-workflow-will-be-triggered-if-there-is-any-commit-is-made-on-branch-develop">The workflow will be triggered if there is any commit is made on branch <code class="language-plaintext highlighter-rouge">develop</code>.</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre>name: CI-Deploy

on:
  push:
    branches:
      - develop
</pre></td></tr></tbody></table></code></pre></div></div>

<h3 id="setup-build-job">Setup build job</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
</pre></td><td class="rouge-code"><pre>jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      app1_exist: $
      app2_exist: $
    steps:
      - name: 🛎️ Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: 📥 Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 20  # Specify your Node.js version
          cache: 'npm'
      - name: 📦 Install dependencies
        run: npm ci
      - name: 🏗️ Build only affected apps
        run: npx nx affected -t build --base=develop~1 --head=develop
      - name: 🔍 Check if app1 build output folder exists
        id: check_build_output1
        if: $
        run: echo "app1_exist=true" &gt;&gt; "$GITHUB_OUTPUT"
      - name: 🔍 Check if app1 build output folder exists
        id: check_build_output2
        if: $
        run: echo "app2_exist=true" &gt;&gt; "$GITHUB_OUTPUT"
      - name: ⬆️ Upload app1 artifact
        if: steps.check_build_output.outputs.app1_exist == 'true'
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/apps/app1
      - name: ⬆️ Upload app2 artifact
        if: steps.check_build_output.outputs.app2_exist == 'true'
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/apps/app2
</pre></td></tr></tbody></table></code></pre></div></div>

<h4 id="steps">Steps</h4>

<ol>
  <li>Checkout the source code</li>
  <li>Setup node.js env</li>
  <li>Install dependencies</li>
  <li>Build affected apps</li>
  <li>Check if output folder of <code class="language-plaintext highlighter-rouge">app1</code> is existed</li>
  <li>Check if output folder of <code class="language-plaintext highlighter-rouge">app2</code> is existed</li>
  <li>Upload artifacts</li>
</ol>

<hr />]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="angular" /><category term="aws" /><category term="github" /><category term="s3" /><category term="monorepo" /><summary type="html"><![CDATA[The guideline is created for Nx monorepo workspace only to build and deploy affected apps to AWS S3 by github workflow.]]></summary></entry><entry><title type="html">Using ChatGPT</title><link href="https://haku-d.github.io/chatgpt/using-chatgpt/" rel="alternate" type="text/html" title="Using ChatGPT" /><published>2024-10-11T13:14:38+00:00</published><updated>2024-10-11T13:14:38+00:00</updated><id>https://haku-d.github.io/chatgpt/using-chatgpt</id><content type="html" xml:base="https://haku-d.github.io/chatgpt/using-chatgpt/"><![CDATA[<p><img src="/assets/DALL·E 2024-10-11 22.28.04.webp" alt="Cover" /></p>

<p>Since ChatGPT’s rise in popularity, it has transformed how new developers approach problem-solving. Rather than searching on Google, they now turn to ChatGPT. Instead of consulting documentation, they ask ChatGPT. And instead of seeking answers from other developers on StackOverflow, they rely on ChatGPT.</p>

<p>However, there are notable disadvantages to relying on ChatGPT for problem-solving. One major concern is the <strong>risk of over-reliance</strong>, where developers might use ChatGPT as a crutch rather than developing critical thinking and problem-solving skills. Instead of learning how to independently research, debug, or fully understand complex concepts, some may become too dependent on instant answers, which could limit their long-term growth as developers.</p>

<p>Another issue is <strong>inaccuracy or incomplete information</strong>. While ChatGPT is highly sophisticated, it is not infallible and can sometimes provide incorrect or outdated solutions. Without proper vetting, developers may implement faulty code or follow suboptimal practices, leading to potential bugs or inefficiencies in their projects.</p>

<p>Additionally, <strong>context limitations</strong> can hinder the effectiveness of ChatGPT in solving more nuanced or project-specific problems. It may not fully grasp the intricacies of a developer’s entire codebase or the unique environment in which they are working, making some of its suggestions less applicable or even harmful if blindly followed.</p>

<p>Finally, there’s the concern of <strong>diminishing community engagement</strong>. Traditionally, developers would turn to communities like StackOverflow or GitHub discussions for help, contributing to a shared knowledge base. As more developers rely on ChatGPT for immediate answers, fewer may engage in these collaborative spaces, potentially leading to a decline in community-driven problem-solving and innovation.</p>

<hr />]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="chatgpt" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Deploy angular application to aws s3 with nx executors</title><link href="https://haku-d.github.io/angular/aws/s3/deploy-angular-to-aws-s3-with-nx-executor/" rel="alternate" type="text/html" title="Deploy angular application to aws s3 with nx executors" /><published>2024-09-26T16:24:38+00:00</published><updated>2024-09-26T16:24:38+00:00</updated><id>https://haku-d.github.io/angular/aws/s3/deploy-angular-to-aws-s3-with-nx-executor</id><content type="html" xml:base="https://haku-d.github.io/angular/aws/s3/deploy-angular-to-aws-s3-with-nx-executor/"><![CDATA[<p>I would love to share the package that I have used to build &amp; deploy my angular application to AWS S3. The source code was inspired by <code class="language-plaintext highlighter-rouge">@jefiozie/ngx-aws-deploy</code>. Since my current application is built in an nx monorepo, I decided to create an nx executor along with the application.</p>

<p>You can found all the source code in folder tools/builder and follow my instructions in this post to configure it on your project.</p>

<h2 id="1-create-an-nx-executor">1. Create an nx executor</h2>

<p>Run this command to create new library</p>

<p><code class="language-plaintext highlighter-rouge">npx nx g @nx/angular:library builder --directory=tools/builder</code></p>

<blockquote>
  <p>Feel free to modify the library name (builder) and its directory(tools/builder)</p>
</blockquote>

<h2 id="2-get-the-source-code">2. Get the source code</h2>

<p>Go to <code class="language-plaintext highlighter-rouge">https://github.com/haku-d/nx-aws-deploy</code> and copy all source files inside folder <code class="language-plaintext highlighter-rouge">tools/builder</code> to your project.</p>

<h2 id="3-integrate-the-executor-into-your-application">3. Integrate the executor into your application</h2>

<p>Open <code class="language-plaintext highlighter-rouge">project.json</code> in your app folder, add new <code class="language-plaintext highlighter-rouge">deploy</code> target as below:</p>

<p>Replace <code class="language-plaintext highlighter-rouge">hello-bucket</code> by your desire bucket name
Replace <code class="language-plaintext highlighter-rouge">&lt;your-cloud-front-distribution-id&gt;</code> by your cloudfront distribution id</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
</pre></td><td class="rouge-code"><pre>{
  "name": "hello",
  "$schema": "../node_modules/nx/schemas/project-schema.json",
  "projectType": "application",
  "prefix": "app",
  "sourceRoot": "hello/src",
  "tags": [],
  "targets": {
    // other target
    "deploy": {
      "executor": "@ng-mf/builder:deploy",
      "options": {
        "bucket": "hello-bucket",
        "cfDistributionId": "&lt;your-cloud-front-distribution-id&gt;",
        "globFileUploadParamsList": [
          {
            "glob": "*",
            "ACL": "public-read",
            "CacheControl": "max-age=3600"
          },
          {
            "glob": "*.html",
            "CacheControl": "max-age=300"
          }
        ]
      }
    }
  }
}
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="4-deploy-your-application">4: Deploy your application</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>npx cross-env NG_DEPLOY_AWS_ACCESS_KEY_ID=1234 NG_DEPLOY_AWS_SECRET_ACCESS_KEY=321ACCESS NG_DEPLOY_AWS_REGION=ca-central-1 nx run hello:deploy
</pre></td></tr></tbody></table></code></pre></div></div>]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="angular" /><category term="aws" /><category term="s3" /><summary type="html"><![CDATA[I would love to share the package that I have used to build &amp; deploy my angular application to AWS S3. The source code was inspired by @jefiozie/ngx-aws-deploy. Since my current application is built in an nx monorepo, I decided to create an nx executor along with the application.]]></summary></entry><entry><title type="html">Lumenalta Technical Assessment</title><link href="https://haku-d.github.io/interview/lumenalta/lumenalta-technical-assessment/" rel="alternate" type="text/html" title="Lumenalta Technical Assessment" /><published>2024-08-18T16:24:38+00:00</published><updated>2024-08-18T16:24:38+00:00</updated><id>https://haku-d.github.io/interview/lumenalta/lumenalta-technical-assessment</id><content type="html" xml:base="https://haku-d.github.io/interview/lumenalta/lumenalta-technical-assessment/"><![CDATA[<p>In March 2024, I got an email from someone at <a href="https://lumenalta.com/">Lumenalta</a> with the link to the technical assessment. It stated that I would have 2 hours to complete the assessment which was real-world coding (<strong>so excited</strong>).</p>

<p>There are two parts to the assessment. The first part is related to the front-end application. It required adding new features, modifying the existing ones, and even updating the unit test. The second part is related to back-end development. In this article, I would like to share what I have done so far on this assessment even though I couldn’t get it done in 2 hours (:sob::sob::sob:). Btw, it was good to have a chance to do the assessment :smile:.</p>

<h3 id="lets-jump-right-into-it">Let’s jump right into it.</h3>
<p>The title of the assessment was called “Javascript: Parallel Asynchronous Programming”. It means that you have to create something that is gonna run <strong>asynchronous</strong> and <strong>parallelly</strong>.</p>

<h3 id="the-objective">The objective</h3>
<p>Your goal is to write an asynchronous function <code class="language-plaintext highlighter-rouge">pooledDownload</code>. This function needs to be able to parallelly download and save files from a given array of URLs, using a connection pool of a given size.</p>

<h3 id="the-interesting-part-was-the-acceptance-criteria">The interesting part was the acceptance criteria</h3>
<ul>
  <li>All files need to be downloaded and saved.</li>
  <li>File contents should be saved as soon as they are downloaded.</li>
  <li>Downloads must be distributed over the connections as evenly as possible.</li>
  <li>One connection can only be downloading one file at a time. You’ll need to create more connections to download multiple files at the same time.</li>
  <li>Any opened connections must always be closed.</li>
  <li>If no connections could be opened to the server, your function needs to reject with a new <code class="language-plaintext highlighter-rouge">Error</code> containing the message <code class="language-plaintext highlighter-rouge">"connection failed"</code>.</li>
  <li>If any error occurred during the transfer, your function needs to reject with this same error.</li>
  <li>BONUS: Sometimes, the server may not have enough slots to accept the requested amount of concurrent connections. In this case, you are to stop opening new connections once the server reaches its capacity and proceed with as many connections as the server allows.</li>
  <li>You can choose whether to use <code class="language-plaintext highlighter-rouge">Promise</code>s, <code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">await</code> or a mixture of them</li>
</ul>

<p>At first, I tried to solve it by using <code class="language-plaintext highlighter-rouge">Promise</code> but I couldn’t get it. Then I switched to <code class="language-plaintext highlighter-rouge">async/await</code> and passed all the unit tests.</p>

<p>The code below reflects what I was able to accomplish after the time expired. I wish I had more time or another opportunity to revisit it, as I found the task truly engaging.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
</pre></td><td class="rouge-code"><pre><span class="cm">/**
 * downloads were distributed evenly among the successful connections
 * @param {*} numberOfRecipients
 * @param {*} totalAmount
 * @returns Array
 *
 * Example: if number of recipients is 4, and totalAmount is 10
 *  -&gt; Output: [3, 3, 2, 2]
 */</span>
<span class="kd">const</span> <span class="nx">distributeEvenly</span> <span class="o">=</span> <span class="p">(</span><span class="nx">numberOfRecipients</span><span class="p">,</span> <span class="nx">totalAmount</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">min</span> <span class="o">=</span> <span class="nb">parseInt</span><span class="p">(</span><span class="nx">totalAmount</span> <span class="o">/</span> <span class="nx">numberOfRecipients</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">remainder</span> <span class="o">=</span> <span class="nx">totalAmount</span> <span class="o">%</span> <span class="nx">numberOfRecipients</span><span class="p">;</span>
  <span class="k">return</span> <span class="nb">Array</span><span class="p">(</span><span class="nx">numberOfRecipients</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">fill</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">_</span><span class="p">,</span> <span class="nx">idx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">min</span> <span class="o">+</span> <span class="p">(</span><span class="nx">idx</span> <span class="o">&lt;</span> <span class="nx">remainder</span> <span class="p">?</span> <span class="mi">1</span> <span class="p">:</span> <span class="mi">0</span><span class="p">));</span>
<span class="p">};</span>

<span class="kd">const</span> <span class="nx">pooledDownload</span> <span class="o">=</span> <span class="k">async</span> <span class="p">(</span><span class="nx">connect</span><span class="p">,</span> <span class="nx">save</span><span class="p">,</span> <span class="nx">downloadList</span><span class="p">,</span> <span class="nx">maxConcurrency</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">connections</span> <span class="o">=</span> <span class="p">[];</span>

  <span class="k">try</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">maxConcurrency</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">connections</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="k">await</span> <span class="nx">connect</span><span class="p">());</span>
    <span class="p">}</span>
  <span class="p">}</span> <span class="k">catch</span> <span class="p">(</span><span class="nx">_</span><span class="p">)</span> <span class="p">{</span>
    <span class="cm">/** Ignore error &amp; stop calling connect to server */</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="nx">connections</span><span class="p">.</span><span class="nx">length</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">"</span><span class="s2">connection failed</span><span class="dl">"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="kd">const</span> <span class="nx">fileDistributed</span> <span class="o">=</span> <span class="nx">distributeEvenly</span><span class="p">(</span>
    <span class="nx">connections</span><span class="p">.</span><span class="nx">length</span><span class="p">,</span>
    <span class="nx">downloadList</span><span class="p">.</span><span class="nx">length</span>
  <span class="p">);</span>

  <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span>
    <span class="nx">connections</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">connection</span><span class="p">,</span> <span class="nx">idx</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="p">{</span> <span class="nx">download</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">connection</span><span class="p">;</span>
      <span class="kd">const</span> <span class="nx">count</span> <span class="o">=</span> <span class="nx">fileDistributed</span><span class="p">[</span><span class="nx">idx</span><span class="p">];</span>
      <span class="kd">const</span> <span class="nx">files</span> <span class="o">=</span> <span class="nx">downloadList</span><span class="p">.</span><span class="nx">splice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">count</span><span class="p">);</span>
      <span class="k">return</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span>
        <span class="nx">files</span><span class="p">.</span><span class="nx">map</span><span class="p">((</span><span class="nx">file</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
          <span class="k">return</span> <span class="nx">download</span><span class="p">(</span><span class="nx">file</span><span class="p">).</span><span class="nx">then</span><span class="p">((</span><span class="nx">result</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">save</span><span class="p">(</span><span class="nx">result</span><span class="p">));</span>
        <span class="p">})</span>
      <span class="p">);</span>
    <span class="p">})</span>
  <span class="p">).</span><span class="k">finally</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">connections</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">conn</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">conn</span><span class="p">.</span><span class="nx">close</span><span class="p">()));</span>
<span class="p">};</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">pooledDownload</span><span class="p">;</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Hope you all enjoy it and good luck if you found one with your search in 2024. In the next post, I will share my journey with another technical assessment which almost took me for three days.</p>]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="interview" /><category term="lumenalta" /><summary type="html"><![CDATA[In March 2024, I got an email from someone at Lumenalta with the link to the technical assessment. It stated that I would have 2 hours to complete the assessment which was real-world coding (so excited).]]></summary></entry><entry><title type="html">Deploy Angular application using Docker</title><link href="https://haku-d.github.io/angular/build-and-run-angular-on-nginx-using-docker/" rel="alternate" type="text/html" title="Deploy Angular application using Docker" /><published>2024-08-13T16:24:38+00:00</published><updated>2024-08-13T16:24:38+00:00</updated><id>https://haku-d.github.io/angular/build-and-run-angular-on-nginx-using-docker</id><content type="html" xml:base="https://haku-d.github.io/angular/build-and-run-angular-on-nginx-using-docker/"><![CDATA[<p>While Angular comes with a number of third-party builders which implement deployment capabilities to different platforms such as Firebase, Vercel, Netlify, AWS S3, and Github pages. But this guide will walk you through how to deploy your angular application to a remote server using docker.</p>

<h2 id="step-1-get-your-angular-application-ready">Step 1: Get your angular application ready</h2>
<p>Before you begin, make sure your angular application is ready for production.</p>

<h2 id="step-2-create-the-dockerfile">Step 2: Create the Dockerfile</h2>

<div class="language-docker highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
</pre></td><td class="rouge-code"><pre><span class="k">FROM</span><span class="w"> </span><span class="s">node:20-alpine</span><span class="w"> </span><span class="k">as</span><span class="w"> </span><span class="s">dist</span>

<span class="k">WORKDIR</span><span class="s"> /app</span>

<span class="c"># Install dependencies</span>
<span class="k">COPY</span><span class="s"> package.json package-lock.json ./</span>
<span class="k">RUN </span>npm <span class="nb">install</span>

<span class="c"># Copy application source code</span>
<span class="k">COPY</span><span class="s"> . .</span>

<span class="c"># Build the application</span>
<span class="k">ARG</span><span class="s"> NODE_ENV=production</span>
<span class="k">RUN </span>npm run build <span class="nt">--configuration</span><span class="o">=</span><span class="k">${</span><span class="nv">NODE_ENV</span><span class="k">}</span>

<span class="k">FROM</span><span class="s"> nginx:alpine-slim</span>

<span class="c"># Copy custom NGINX configuration</span>
<span class="k">COPY</span><span class="s"> nginx.conf /etc/nginx/conf.d/default.conf</span>

<span class="k">WORKDIR</span><span class="s"> /usr/share/nginx/html</span>

<span class="k">COPY</span><span class="s"> --from=dist app/dist/my-app/browser .</span>

<span class="c"># Rename the main HTML file if necessary</span>
<span class="k">RUN </span><span class="nb">mv </span>index.csr.html index.html
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="step-3-create-custom-nginx-configuration-file">Step 3: Create custom nginx configuration file</h2>

<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
</pre></td><td class="rouge-code"><pre><span class="k">server</span> <span class="p">{</span>
  <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
  <span class="kn">sendfile</span> <span class="no">on</span><span class="p">;</span>
  <span class="kn">default_type</span> <span class="nc">application/octet-stream</span><span class="p">;</span>

  <span class="kn">gzip</span> <span class="no">on</span><span class="p">;</span>
  <span class="kn">gzip_http_version</span> <span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
  <span class="kn">gzip_disable</span>      <span class="s">"MSIE</span> <span class="s">[1-6]</span><span class="err">\</span><span class="s">."</span><span class="p">;</span>
  <span class="kn">gzip_min_length</span>   <span class="mi">256</span><span class="p">;</span>
  <span class="kn">gzip_vary</span>         <span class="no">on</span><span class="p">;</span>
  <span class="kn">gzip_proxied</span>      <span class="s">expired</span> <span class="s">no-cache</span> <span class="s">no-store</span> <span class="s">private</span> <span class="s">auth</span><span class="p">;</span>
  <span class="kn">gzip_types</span>        <span class="nc">text/plain</span> <span class="nc">text/css</span> <span class="nc">application/json</span> <span class="nc">application/javascript</span> <span class="nc">application/x-javascript</span> <span class="nc">text/xml</span> <span class="nc">application/xml</span> <span class="nc">application/xml</span><span class="s">+rss</span> <span class="nc">text/javascript</span><span class="p">;</span>
  <span class="kn">gzip_comp_level</span>   <span class="mi">9</span><span class="p">;</span>

  <span class="kn">root</span> <span class="n">/usr/share/nginx/html</span><span class="p">;</span>

  <span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
    <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="n">/index.html</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="kn">location</span> <span class="p">~</span><span class="sr">*</span> <span class="err">\</span><span class="s">.(?:jpg|jpeg|gif|png)</span>$ <span class="p">{</span>
    <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="n">/assets/No-Image-Placeholder.png</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You might be found same guideline on the internet or by asking ChatGPT. But it did not tell you to create custom nginx configuration. Let’s take a look at this case: once your application was deployed successfully and running on port <strong>4200</strong>. You open it on your favourite browser, click on any router link to access other pages, such as /profile. The full url will be <code class="language-plaintext highlighter-rouge">http://localhost:4200/profile</code>.</p>

<p>If you used default nginx configuration for your application and you refresh the browser while opening <code class="language-plaintext highlighter-rouge">http://localhost:4200/profile</code>, nginx will show 404 page. But with this configuration, the nginx server will return the application’s host page (index.html) when asked for a file that it does not have. All routes must fall back to index.html</p>

<h2 id="step-4-build-the-docker-image">Step 4: Build the Docker Image</h2>

<p>To build the Docker image, navigate to the directory containing your Dockerfile and run:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker build -t my-angular-app .
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This command will package your Angular application into a Docker image.</p>

<p>Step 5: Run the Docker Container
After the image is built, you can run it with the following command:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>docker run -p 80:80 my-angular-app
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This command starts an NGINX server inside a Docker container, serving your Angular application on <code class="language-plaintext highlighter-rouge">http://localhost</code>.</p>

<p>Thanks for your reading</p>]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="angular" /><summary type="html"><![CDATA[While Angular comes with a number of third-party builders which implement deployment capabilities to different platforms such as Firebase, Vercel, Netlify, AWS S3, and Github pages. But this guide will walk you through how to deploy your angular application to a remote server using docker.]]></summary></entry><entry><title type="html">Generate pdf with Serverless API - AWS Lambda Function using Puppeteer</title><link href="https://haku-d.github.io/aws/generate-pdf-with-serverless-api/" rel="alternate" type="text/html" title="Generate pdf with Serverless API - AWS Lambda Function using Puppeteer" /><published>2024-08-11T13:14:38+00:00</published><updated>2024-08-11T13:14:38+00:00</updated><id>https://haku-d.github.io/aws/generate-pdf-with-serverless-api</id><content type="html" xml:base="https://haku-d.github.io/aws/generate-pdf-with-serverless-api/"><![CDATA[<p><img src="/assets/generate-pdf-with-serverless-api.png" alt="Cover" /></p>

<p>I started with <code class="language-plaintext highlighter-rouge">PDFKit</code> and <code class="language-plaintext highlighter-rouge">jsPDF</code> to generate pdf but it became difficult with complex layouts. <code class="language-plaintext highlighter-rouge">jsPDF</code> supports generating a PDF from an HTML element or string but it does not work in node environment.</p>

<p>Finally, I ended up using <code class="language-plaintext highlighter-rouge">puppeteer</code> to generate a PDF page from an HTML string. I created a simple <a href="https://github.com/haku-d/html-2-pdf">code</a> using <code class="language-plaintext highlighter-rouge">NestJS</code> framework, including a <code class="language-plaintext highlighter-rouge">POST</code> endpoint which will generate a PDF from HTML string input.</p>

<p>Recently, I found <a href="https://github.com/shelfio/chrome-aws-lambda-layer">Google Chrome for AWS Lambda as a layer</a>. That means I can setup a serverless api with <code class="language-plaintext highlighter-rouge">Lambda</code> running <code class="language-plaintext highlighter-rouge">puppeteer</code>. <strong>THAT IS ALL WE NEED!</strong>.</p>

<p>You can check out the set up with AWS SAM CLI on my github repo: <a href="https://github.com/haku-d/html-2-pdf-lambda">https://github.com/haku-d/html-2-pdf-lambda</a></p>

<hr />]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="aws" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Unlock Tailwind UI Components</title><link href="https://haku-d.github.io/tailwind/unlock-tailwind-components/" rel="alternate" type="text/html" title="Unlock Tailwind UI Components" /><published>2024-08-11T13:14:38+00:00</published><updated>2024-08-11T13:14:38+00:00</updated><id>https://haku-d.github.io/tailwind/unlock-tailwind-components</id><content type="html" xml:base="https://haku-d.github.io/tailwind/unlock-tailwind-components/"><![CDATA[<h3 id="i-enjoy-my-free-time-exploring-tailwind-and-making-copies-of-their-ui-components">I enjoy my free time exploring Tailwind and making copies of their <a href="https://tailwindui.com/components">UI Components</a></h3>

<blockquote>
  <p>Currently, only React is supported</p>
</blockquote>

<p>My work can be found at <a href="https://github.com/haku-d/tw-decoded">https://github.com/haku-d/tw-decoded</a></p>

<p><a classname="bg-sky-500 hover:bg-sky-700 px-5 py-2 text-sm leading-5 rounded-full font-semibold text-white" href="https://tw-decoded.vercel.app/">View Demo</a></p>

<h3 id="todos">TODOS</h3>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />Application Shells</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Forms (WIP)</strong></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Lists</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />Navigations</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />More</li>
</ul>]]></content><author><name>Thanh Dang (Haku)</name><email>dcongthanh@gmail.com</email></author><category term="tailwind" /><summary type="html"><![CDATA[WIP - A free copy of Tailwind UI Components]]></summary></entry></feed>