<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Daydream's Reverie]]></title><description><![CDATA[Contemplative thoughts, engaging stories, and creative ideas]]></description><link>https://sophiecao.me/</link><image><url>https://sophiecao.me/favicon.png</url><title>Daydream&apos;s Reverie</title><link>https://sophiecao.me/</link></image><generator>Ghost 5.76</generator><lastBuildDate>Mon, 09 Mar 2026 04:20:57 GMT</lastBuildDate><atom:link href="https://sophiecao.me/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Responsible AI use - my two cents]]></title><description><![CDATA[<p>AI has been a buzzword for many years and is gaining more public attention. However the word AI is too broad to describe today&apos;s technology advancements. Architecture like Large Language Model (LLM), Large Visual Model (LVM), or even  Reasoning Language Model (RLM) still doesn&apos;t meet the</p>]]></description><link>https://sophiecao.me/responsible-ai-use-my-two-cents/</link><guid isPermaLink="false">6962cd7de3403d000136ab04</guid><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Sat, 10 Jan 2026 22:27:08 GMT</pubDate><content:encoded><![CDATA[<p>AI has been a buzzword for many years and is gaining more public attention. However the word AI is too broad to describe today&apos;s technology advancements. Architecture like Large Language Model (LLM), Large Visual Model (LVM), or even  Reasoning Language Model (RLM) still doesn&apos;t meet the definition of AI, despite their <em>(the reason I avoid &#x201C;it&#x201D; is that &#x201C;they/them&#x201D; feels more appropriate, like how many people now avoid referring to animals as &#x201C;it.&#x201D;)</em> &quot;intelligent&quot; behaviour, which creates the illusion.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sophiecao.me/content/images/2026/01/image.png" class="kg-image" alt loading="lazy" width="2000" height="743" srcset="https://sophiecao.me/content/images/size/w600/2026/01/image.png 600w, https://sophiecao.me/content/images/size/w1000/2026/01/image.png 1000w, https://sophiecao.me/content/images/size/w1600/2026/01/image.png 1600w, https://sophiecao.me/content/images/2026/01/image.png 2000w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">A Google Trends line chart showing global search interest for the term &#x201C;AI&#x201D; from 2004 to 2026. Interest remains very low and flat for most years, then rises sharply starting around 2022, peaking near 2025 before slightly declining.</span></figcaption></figure><p>So what is responsible AI use? In this post I will focus on how we, as individuals, can use AI responsibly. Although I do agree corporations are responsible to the social and environmental impact of using and promoting AI, but the consequence for individual users are more immediate.</p><p>It&apos;s no different from responsible drinking, responsible gaming, and responsible substance use, the essence is, we bear the consequence of our own AI use.</p><p>AI won&apos;t assume liability, at least for now. They are still an algorithm that requires massive computing power to predict the next word (token) in a response. Regardless of the guardrail or how carefully you write your prompt, they can still hallucinate and giving you the seemingly correct but wrong answers.</p><p>AI is bad at recalling context, at least for now. They are still struggling to memorize every word you have given, although we have developed various solutions like RAG or agentic retrieval, they don&apos;t have persistent memory and often drop the details as conversation grows.</p><p>AI can make mistakes, consider checking important information, at least for now. You may notice this under the input box on ChatGPT&apos;s interface. Training data can be outdated, imperfect query terms can be used when they search the internet on your behalf. Nevertheless, they will still give you a seemingly correct answer.</p><p>How can we be responsible? First, we should understand their limitations, and we should avoid overly relying on them. For example, it&apos;s probably fine to ask AI for a recipe, if you can bear the consequences of getting overcooked dinner.</p><p>But there could be more consequences in some industries. Imagine a <em>vibe doctor</em> who uploads your X-ray to an AI and instantly diagnoses you with cancer. A v<em>ibe lawyer</em> whose entire defence strategy is generated by AI without checking. A <em>vibe accountant</em> instructing an AI agent to file your taxes and signing off immediately...and in a broader context, this could erode trust and distort public discussion. Posts like <em>ChatGPT told me that</em>, or <em>Gemini said this is incorrect</em> should absolutely be avoided, and commenting <em>@grok is this true?</em> should be disdained by society, unless you are being sarcastic.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sophiecao.me/content/images/2026/01/image-1.png" class="kg-image" alt loading="lazy" width="1024" height="1536" srcset="https://sophiecao.me/content/images/size/w600/2026/01/image-1.png 600w, https://sophiecao.me/content/images/size/w1000/2026/01/image-1.png 1000w, https://sophiecao.me/content/images/2026/01/image-1.png 1024w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">AI generated image of four people crowd around a smartphone, staring intently at the screen. The phone displays a robot-like AI avatar with glowing eyes and a speech bubble that reads, &#x201C;Is it true? @grok,&#x201D; highlighting people seeking validation from an AI.</span></figcaption></figure><p>Even worse, some people fabricate facts or quote legal clauses out of nowhere just to support their claims, although this is not news, the advancement of technology certainly makes it easier. These irresponsible usage also force non-user to take on the burden of fact checking, but how can we compete with an algorithm that types faster than most humans.</p><p>Responsible AI use means the user has to assume responsibility upon using them. Just like we shouldn&apos;t drive after consuming alcohol or marijuana. We shouldn&apos;t copy and paste AI-generated answers into a textbox without fact-checking. Nor should we claim something works after followed instructions provided by an AI without knowing why. It&apos;s like &quot;I drove fine last time after drinking, so it must be safe&quot;. When stepping into unknown realms, your judgement is already impaired&#x2014;so get yourself &#x201C;sober&#x201D; with some fundamentals first. <em>(oh em dash&#x2014;caught you. Yeah that sentence is AI-generated, and this too)</em>.</p><p>AI generates answers, but not <em>the answer</em>, they have never been a silver bullet, unless we eventually create an omnipotent one and maintain a good enough relationship, but that is another story. Until then, the responsibility is with us, the end users.</p>]]></content:encoded></item><item><title><![CDATA[Counting-out game]]></title><description><![CDATA[<h2 id="mandarin">Mandarin</h2><p>&#x70B9;&#x70B9;&#x8C46;&#x8C46;&#xFF0C;&#x7C73;&#x7CAE;&#x4E8C;&#x6597;&#xFF0C;<br>&#x548C;&#x5C1A;&#x4E0D;&#x5728;&#xFF0C;&#x8BF7;&#x4F60;&#x5148;&#x8D70;&#x3002;</p><h2 id="english">English</h2><p>Eeny, meeny, miny, moe,<br>Catch a tiger by the toe;<br>If he hollers, let him go,<br>Eeny, meeny, miny, moe.</p><h2 id="french">French</h2><p>Un, deux, trois, quatre,<br>Ma petite vache</p>]]></description><link>https://sophiecao.me/counting-out-game/</link><guid isPermaLink="false">68fed392e3403d000136aaf9</guid><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Mon, 27 Oct 2025 02:08:29 GMT</pubDate><content:encoded><![CDATA[<h2 id="mandarin">Mandarin</h2><p>&#x70B9;&#x70B9;&#x8C46;&#x8C46;&#xFF0C;&#x7C73;&#x7CAE;&#x4E8C;&#x6597;&#xFF0C;<br>&#x548C;&#x5C1A;&#x4E0D;&#x5728;&#xFF0C;&#x8BF7;&#x4F60;&#x5148;&#x8D70;&#x3002;</p><h2 id="english">English</h2><p>Eeny, meeny, miny, moe,<br>Catch a tiger by the toe;<br>If he hollers, let him go,<br>Eeny, meeny, miny, moe.</p><h2 id="french">French</h2><p>Un, deux, trois, quatre,<br>Ma petite vache a mal aux pattes.<br>Tire-la par la queue,<br>Elle ira mieux dans un jour ou deux.</p>]]></content:encoded></item><item><title><![CDATA[When Domain Connect fails - a workaround to add DNS records manually]]></title><description><![CDATA[<p>Update: the issue has been resolved.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.cloudflarestatus.com/incidents/xq2wgjp47dm3?ref=sophiecao.me"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Cloudflare Domain Connect Issues</div><div class="kg-bookmark-description">Cloudflare&#x2019;s Status Page - Cloudflare Domain Connect Issues.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://dka575ofm4ao0.cloudfront.net/pages-favicon_logos/original/7809/J2LlHqT3qJl0bG9Alpgc" alt><span class="kg-bookmark-author">Cloudflare Domain Connect Issues</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://dka575ofm4ao0.cloudfront.net/assets/logos/favicon-2b86ed00cfa6258307d4a3d0c482fd733c7973f82de213143b24fc062c540367.png" alt></div></a></figure><p>When I was trying to add some DNS records to enable an iCloud custom email domain, following the instructions in iCloud settings and being redirected to</p>]]></description><link>https://sophiecao.me/when-domain-connect-fails-a-workaround-to-add-dns-records-manually/</link><guid isPermaLink="false">66a0300ac1e8f00001e2e781</guid><category><![CDATA[DNS]]></category><category><![CDATA[Cloudflare]]></category><category><![CDATA[iCloud]]></category><category><![CDATA[Domain Connect]]></category><category><![CDATA[Email]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Tue, 23 Jul 2024 22:37:45 GMT</pubDate><media:content url="https://sophiecao.me/content/images/2024/07/Capture-d--cran--le-2024-07-23---18.48.24.png" medium="image"/><content:encoded><![CDATA[<img src="https://sophiecao.me/content/images/2024/07/Capture-d--cran--le-2024-07-23---18.48.24.png" alt="When Domain Connect fails - a workaround to add DNS records manually"><p>Update: the issue has been resolved.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.cloudflarestatus.com/incidents/xq2wgjp47dm3?ref=sophiecao.me"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Cloudflare Domain Connect Issues</div><div class="kg-bookmark-description">Cloudflare&#x2019;s Status Page - Cloudflare Domain Connect Issues.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://dka575ofm4ao0.cloudfront.net/pages-favicon_logos/original/7809/J2LlHqT3qJl0bG9Alpgc" alt="When Domain Connect fails - a workaround to add DNS records manually"><span class="kg-bookmark-author">Cloudflare Domain Connect Issues</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://dka575ofm4ao0.cloudfront.net/assets/logos/favicon-2b86ed00cfa6258307d4a3d0c482fd733c7973f82de213143b24fc062c540367.png" alt="When Domain Connect fails - a workaround to add DNS records manually"></div></a></figure><p>When I was trying to add some DNS records to enable an iCloud custom email domain, following the instructions in iCloud settings and being redirected to Cloudflare, I encountered the following error:</p><blockquote>Cloudflare cannot proceed with applying new DNS records for iCloud because this URL is not verified.</blockquote><p>After several attempts, I discovered that others were experiencing the same issue:</p><p><a href="https://community.cloudflare.com/t/custom-domain-with-icloud-url-is-not-verified/689204?ref=sophiecao.me">https://community.cloudflare.com/t/custom-domain-with-icloud-url-is-not-verified/689204</a></p><p>However, I then noticed that this workflow of adding DNS records is based on an open standard called <a href="https://www.domainconnect.org/?ref=sophiecao.me" rel="noreferrer">Domain Connect</a>, and the template for service providers to apply their configuration is publicly available. The template for configuring the iCloud custom email domain is available <a href="https://github.com/Domain-Connect/Templates/blob/master/icloud.com.custom_email_domain.json?ref=sophiecao.me">here</a>.</p><p>Now we already have the template, and we need to know how the Domain Connect works. in this case, iCloud uses a <a href="https://github.com/Domain-Connect/spec/blob/master/Domain%20Connect%20Spec%20Draft.adoc?ref=sophiecao.me#synchronous-flow">synchronous flow</a> to guide users through applying the required DNS changes. Based on this flow, we can complete the necessary steps manually.</p><h2 id="decoding-the-url"><strong>Decoding the URL</strong></h2><pre><code>https://dash.cloudflare.com/domainconnect/v2/domainTemplates/providers/icloud.com/services/custom_email_domain/apply?domain=[redacted]&amp;state=[redacted]&amp;redirect_uri=https%3A%2F%2Fgateway.icloud.com%2Fmaildomainws%2Fv1%2Fdomain%2FdomainConnectReply&amp;mx1_data=mx01.mail.icloud.com.&amp;mx2_data=mx02.mail.icloud.com.&amp;icloud_token=[redacted]&amp;dkim_host=sig1._domainkey&amp;dkim_data=[redacted]&amp;spfm_data=include%3Aicloud.com&amp;sig=[redacted]&amp;key=[redacted]</code></pre><p>Above is the URL generated by iCloud for us to complete the setup. However, when visiting this URL, it gives us the error above. However, if we look at the URL parameters, we can easily find all the information we need. Let&apos;s start by decoding the URL first:</p><pre><code>https://dash.cloudflare.com/domainconnect/v2/domainTemplates/providers/icloud.com/services/custom_email_domain/apply?domain=[redacted]
&amp;state=[redacted]
&amp;redirect_uri=https://gateway.icloud.com/maildomainws/v1/domain/domainConnectReply
&amp;mx1_data=mx01.mail.icloud.com.
&amp;mx2_data=mx02.mail.icloud.com.
&amp;icloud_token=[redacted]
&amp;dkim_host=sig1._domainkey
&amp;dkim_data=[redacted]
&amp;spfm_data=include:icloud.com
&amp;sig=[redacted]
&amp;key=[redacted]</code></pre><h2 id="applying-dns-changes-based-on-the-template"><strong>Applying DNS changes based on the template</strong></h2><p>Now that we have obtained the parameters from the last step, we can replace the template string and attempt to construct DNS records. Most record types are straightforward; however, for the <code>SPFM</code> type, we need to consult the documentation.</p><p>The document provides an <a href="https://github.com/Domain-Connect/spec/blob/master/Domain%20Connect%20Spec%20Draft.adoc?ref=sophiecao.me#example-spf-merge">example</a> of the <code>SPFM</code> record type. Meaning we should construct the <code>SPF</code> record as follows:</p><pre><code>&#xA0;v=spf1 %{spfm_data} ~all</code></pre><h2 id="tell-icloud-that-dns-changes-were-complete"><strong>Tell iCloud that DNS changes were complete</strong></h2><p>After we added all the DNS records, it was time to tell iCloud that changes were applied and the verification process could be started. This step is necessary because iCloud specified the <code>redirect_uri</code>. Now let&apos;s look at the <a href="https://github.com/Domain-Connect/spec/blob/master/Domain%20Connect%20Spec%20Draft.adoc?ref=sophiecao.me#same-browser-window">document</a> again. </p><blockquote>The second is in the current browser tab/window. As above the DNS Provider signs the user in if necessary, verifies the user control of the DNS Zone for the domain, and asks for confirmation before application of the template. After application of the template (or cancellation by the user), the DNS Provider must redirect the browser to a return URL (redirect_uri).</blockquote><blockquote>Several parameters must be appended to the end of this redirect_uri.StateIf a state parameter is passed in on the query string, this must be passed back as state= on the redirect_uri.</blockquote><p>Therefore, we were able to construct the <code>redirect_url</code> in the above format, which in this specific case is:</p><pre><code>&#xA0;https://gateway.icloud.com/maildomainws/v1/domain/domainConnectReply?state=[redacted]</code></pre><p>Upon visiting the URL, we noticed that iCloud successfully initiated the verification process, and the custom domain email was added successfully.</p>]]></content:encoded></item><item><title><![CDATA[Marriage: the goal of romantic relationships?]]></title><description><![CDATA[<p>Love is love, it is a true feeling and a state of mind. Love doesn&apos;t matter with whom you are with, it doesn&apos;t matter with how you express it. It doesn&apos;t need a definition. Nor do people need to argue about what is love.</p>]]></description><link>https://sophiecao.me/marriage-the-goal-of-romantic-relationships/</link><guid isPermaLink="false">662487ecc1e8f00001e2e6ff</guid><category><![CDATA[Miscellaneous]]></category><category><![CDATA[寻找最佳实践]]></category><category><![CDATA[Finding best practices]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Sun, 21 Apr 2024 03:57:28 GMT</pubDate><content:encoded><![CDATA[<p>Love is love, it is a true feeling and a state of mind. Love doesn&apos;t matter with whom you are with, it doesn&apos;t matter with how you express it. It doesn&apos;t need a definition. Nor do people need to argue about what is love. As long as it feels right, and has consent, it is love.</p><p>Two weeks ago, I was at a communion in a Chinese church, having an interesting debate as the counterclaim about whether the goal of romantic relationships should be marriage (&#x8AC7;&#x6200;&#x611B;&#x61C9;&#x4E0D;&#x61C9;&#x8A72;&#x4EE5;&#x7D50;&#x5A5A;&#x70BA;&#x76EE;&#x7684;). The time was very limited and I didn&apos;t elaborate it well. And that is the reason why I wrote this post. This post specifically discussed love in a heterosexual relationship but there is something we could learn even in other forms of relationship.</p><h2 id="matrilineal-human-kinship">Matrilineal human kinship</h2><p>Early human kinship was matrilineal, in which the inheritance was traced through the mother&apos;s side of the family. Eventually, as society became complex and women&apos;s power was reduced due to agricultural practices, patriarchy started to form, in which men held the power and dominated all, at this time, the system of marriage emerged, and it became a way for the family to be bounded together, ensuring the child was born legitimately, in the other hand, controlling a women&apos;s reproductive rights. <strong>It solidifies the patriarchal societies and reinforces the power structure and the gender roles,</strong> women were even been seen as a form of property to be exchanged between families (which is even seen in some societies today).</p><h2 id="what-marriage-carries">What marriage carries</h2><p>So, what benefits does marriage bring, and what is the meaning of marriage? Since marriage was derived from the patriarchy, it is inherently patriarchal, making it largely unproportional when speaking to the benefit of marriage between man and woman, even in the modern-day context.</p><p>Marriage is a contract, it is a legal binding between two people to form a partnership and to abide by the marriage laws and regulations. In addition, marriage is often associated with the social norms and expectations that the couples are expected to follow and might face criticism, judgement or accusations from society if they deviate. Therefore, marriage has consequences, and it largely depends on the context of law and norms in society.</p><h2 id="marriage-is-a-kidnapper">Marriage is a kidnapper</h2><p>Although marriage is a commitment, it is not the <strong>only one and the ultimate one</strong>. Since the reality of marriage is cruel, in today&apos;s context, marriage kidnaps love. People have to pretend they still love each other, and they cannot easily leave even if there is no love. So, how come marriage becomes the goal of a romantic relationship?</p><p>Over the decades, we have used a filter when considering marriage. We think marriage is beautiful, sacred, and romantic. However, it is romantic only when love is inside. It is not and can&apos;t be when love is not in there. Not only the motion for love is kidnapped, but the concept of love is kidnapped as well.</p><p>We all want certainties, and we are trying to use marriage to seal the uncertainties of love, even when we don&apos;t figure out whether it is the right person or not. we are too impatient to get that certainty, but marriage is not the silver bullet, it has its problems to be solved as well.</p><h2 id="the-essence-of-a-romantic-relationship">The essence of a romantic relationship</h2><p>A romantic relationship, in the beginning, starts with the chemical in mind, giving the passion of wanting to connect. We cannot help to fall in love, and then we dive into love, seeking for deeper connection. However, as time went by and the influence of chemistry faded, we began to use more of our rationale. We started determining whether we truly loved the person or not.</p><p>When a relationship starts, it doesn&apos;t need a reason, nor does it need an ultimate goal. Relationships are pure exploring, a voyage, a connection between souls. It contains happiness, and bitterness and is unexpected. Relationships don&apos;t need assumptions, it simply is the willingness of wanting to spend time with others and wish all the best to happen to others. Relationships are all about whether it feels right.</p><p>Now, talking about the consequence of having marriage as the ultimate goal of a romantic relationship, we gave a prophecy, and even if it is achieved, it is self-proven. We are strangled by this prophecy. We compromise, we hide, not able to display our personalities. We try to avoid conflicts, but eventually, it will re-emerge. We planted a timed bomb, and more time was &quot;wasted&quot; when trying to find the right person.</p><h2 id="set-love-free-and-let-love-be-love">Set love free and let love be love</h2><p>Love should be free because we always want the best to happen to the one we love. And what is more valuable than freedom? Love should have the freedom to enter or quit when it is best in everyone&apos;s interest. Because every lover deserves to have the best love in the world.</p><p>Love is selfless and love sometimes is giving up, because we are all independent individuals in the middle of our life journey. Love is openness and honesty, we should address each other when love is moving apart. Love is among the highest. And it should never be kidnapped by anyone&apos;s interest.</p><p>Love is compromise, but this compromise should never involve compromising love itself. And love is colourful, love doesn&apos;t matter about gender, orientation or race. Every form of love should be affirmed, as long as it is consented between two able-to-make-decision individuals.</p><p>Please, let love be love!</p><h2 id="extensive-reading-bible-verses-about-marriage"><em>Extensive reading: Bible verses about marriage</em></h2><blockquote><em>Wives, submit to your own husbands, as to the Lord. For the husband is the head of the wife even as Christ is the head of the church, his body, and is himself its Savior.</em> (Ephesians 5:22&#x2013;24)<br><br><em>It is not good that the man should be alone; I will make him a helper fit for him</em> (Genesis 2:18). <br><br><em>Husbands, in the same way, be considerate as you live with your wives, and treat them with respect as the weaker partner and as heirs with you of the gracious gift of life, so that nothing will hinder your prayers</em> (1 Peter 3:7).</blockquote><p>In those bible verses, we got this submissive wife and protective husband stereotype, and women were degraded as a belonging, a property, limiting their autonomy and agency, as well as undermining their equality in a partnership. Reinforcing the gender roles and limiting the ability (we know those were limited by the historical background, with a very patriarchal system and most of the authors were male).</p><p>However, what cannot be denied is that many men still hold the same belief (as of the writer&apos;s experience), regardless of whether they consciously or unconsciously realize, which should be alerted.</p>]]></content:encoded></item><item><title><![CDATA[Quebec French profanity]]></title><description><![CDATA[In Cantonese, we have the word 粵韻風華 (the elegance of Cantonese) to refer to words and expressions of profanity words. The same thing also exists in Quebecois, which the people refer to as sacres (to consecrate).]]></description><link>https://sophiecao.me/quebec-french-profanity/</link><guid isPermaLink="false">65eb6c12c1e8f00001e2e6df</guid><category><![CDATA[French]]></category><category><![CDATA[Miscellaneous]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Fri, 08 Mar 2024 19:51:49 GMT</pubDate><media:content url="https://sophiecao.me/content/images/2024/03/Untitled-4.png" medium="image"/><content:encoded><![CDATA[<figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/Hy-XFqkgSEY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Osti de crisse de tabarnak avec paroles"></iframe></figure><img src="https://sophiecao.me/content/images/2024/03/Untitled-4.png" alt="Quebec French profanity"><p>In Cantonese, we have the word <em>&#x7CB5;&#x97FB;&#x98A8;&#x83EF; (the elegance of Cantonese)</em> to refer to words and expressions of profanity words. The same thing also exists in Quebecois, which the people refer to as <em>sacres (to consecrate)</em>.</p><table>
<thead>
<tr>
<th>Word</th>
<th>Original Meaning (English)</th>
<th>Meaning (English)</th>
</tr>
</thead>
<tbody>
<tr>
<td>osti</td>
<td>sacramental bread</td>
<td>damn / fuckin</td>
</tr>
<tr>
<td>crisse</td>
<td>christ</td>
<td></td>
</tr>
<tr>
<td>tabarnak</td>
<td>tabernacle</td>
<td>damn / fuckin</td>
</tr>
<tr>
<td>c&#xE2;lisse</td>
<td>chalice</td>
<td>damn / fuckin</td>
</tr>
<tr>
<td>viarge</td>
<td>virgin (Virgin Mary)</td>
<td>surprised / swear (seldom)</td>
</tr>
<tr>
<td>calvaire</td>
<td>calvery</td>
<td>fuckin / bloody hale</td>
</tr>
<tr>
<td>ciboire / siboire</td>
<td>ciborium</td>
<td>damn / fuckin</td>
</tr>
<tr>
<td>sacres</td>
<td>to consecrate</td>
<td>profanities</td>
</tr>
<tr>
<td>Saint-osti</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
<h2 id="read-more">Read more</h2><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://fr.wikipedia.org/wiki/Sacre_qu%C3%A9b%C3%A9cois?ref=sophiecao.me#Liste_de_sacres_et_leur_signification"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Sacre qu&#xE9;b&#xE9;cois &#x2014; Wikip&#xE9;dia</div><div class="kg-bookmark-description"></div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://fr.wikipedia.org/static/apple-touch/wikipedia.png" alt="Quebec French profanity"><span class="kg-bookmark-author">Fondation Wikimedia, Inc.</span><span class="kg-bookmark-publisher">Contributeurs aux projets Wikimedia</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Logo_disambig.svg/20px-Logo_disambig.svg.png" alt="Quebec French profanity"></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Login to Strapi's Admin Dashboard using the API Token]]></title><description><![CDATA[<blockquote>This post was inspired by <a href="https://forum.strapi.io/t/access-admin-panel-using-url-with-token/20578?ref=sophiecao.me" rel="noreferrer">the discussion</a> in the Strapi Forum.</blockquote><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://stanleytsau.me/content/media/2024/03/Enregistrement-d--cran--le-2024-03-04---16.01.32_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://sophiecao.me/content/media/2024/03/Enregistrement-d--cran--le-2024-03-04---16.01.32.mp4" poster="https://img.spacergif.org/v1/1369x1080/0a/spacer.png" width="1369" height="1080" playsinline preload="metadata" style="background: transparent url(&apos;https://sophiecao.me/content/media/2024/03/Enregistrement-d--cran--le-2024-03-04---16.01.32_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:12</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><h2 id="part-1-create-the-endpoint-to-get-user-info-and-jwt-token">Part 1: Create the endpoint to get user info and JWT token.</h2><p>In the original discussion, it was achieved by passing a JWT token and user info as the query param to log</p>]]></description><link>https://sophiecao.me/login-to-strapi-admin-dashboard-using-the-api-token/</link><guid isPermaLink="false">65e62f5be05771000106c1ba</guid><category><![CDATA[Backend]]></category><category><![CDATA[Frontend]]></category><category><![CDATA[Strapi]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Mon, 04 Mar 2024 20:57:17 GMT</pubDate><content:encoded><![CDATA[<blockquote>This post was inspired by <a href="https://forum.strapi.io/t/access-admin-panel-using-url-with-token/20578?ref=sophiecao.me" rel="noreferrer">the discussion</a> in the Strapi Forum.</blockquote><figure class="kg-card kg-video-card kg-width-regular" data-kg-thumbnail="https://stanleytsau.me/content/media/2024/03/Enregistrement-d--cran--le-2024-03-04---16.01.32_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://sophiecao.me/content/media/2024/03/Enregistrement-d--cran--le-2024-03-04---16.01.32.mp4" poster="https://img.spacergif.org/v1/1369x1080/0a/spacer.png" width="1369" height="1080" playsinline preload="metadata" style="background: transparent url(&apos;https://sophiecao.me/content/media/2024/03/Enregistrement-d--cran--le-2024-03-04---16.01.32_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:12</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            
        </figure><h2 id="part-1-create-the-endpoint-to-get-user-info-and-jwt-token">Part 1: Create the endpoint to get user info and JWT token.</h2><p>In the original discussion, it was achieved by passing a JWT token and user info as the query param to log in to the admin user. But what if we want to login using a static token generated inside the admin dashboard?</p><p>First, we need to create a new API using the following command.</p><pre><code class="language-bash">yarn strapi generate</code></pre><p>On the generated files, change <code>src/api/teleport/routes/teleport.js</code></p><pre><code class="language-javascript">module.exports = {
  routes: [
    {
      method: &apos;GET&apos;,
      path: &apos;/teleport&apos;,
      handler: &apos;teleport.login&apos;,
      config: {
        policies: [],
        middlewares: [],
      },
    },
  ],
};</code></pre><p>, then add the functionality in <code>src/api/teleport/controllers/teleport.js</code></p><pre><code class="language-javascript">&apos;use strict&apos;;

/**
 * A set of functions called &quot;actions&quot; for `teleport`
 */

const ADMIN_EMAIL = &apos;admin@example.com&apos;

module.exports = {
   login: async (ctx, next) =&gt; {
    const hasToken = ctx.request.query &amp;&amp; ctx.request.query.token;
    const token = hasToken ? ctx.request.query.token : null;

    if (!hasToken) {
      ctx.status = 404;
      return;
    }

    // check token
    const apiTokenService = strapi.services[&apos;admin::api-token&apos;];
    const accessKey = await apiTokenService.hash(token);
    const storedToken = await apiTokenService.getBy({accessKey: accessKey});

    if (!storedToken) {
      ctx.status = 404;
      return;
    }

    // Deny access if expired.
    if (storedToken.expiresAt &amp;&amp; storedToken.expiresAt &lt; new Date()) {
      ctx.status = 404;
      return;
    }

    // only full-access token
    if (storedToken.type != &apos;full-access&apos;) {
      ctx.status = 404;
      return;
    }

    // get admin user
    const user = await strapi.db.query(&apos;admin::user&apos;).findOne({
      where: {
        email: ADMIN_EMAIL
      }
    })

    // hide sensitive data
    delete user.password;
    delete user.resetPasswordToken;
    delete user.registrationToken;

    const jwtToken = strapi.plugins[&apos;users-permissions&apos;].services.jwt.issue({
      id: user.id
    });

    const payload = JSON.stringify({
      user: user,
      jwtToken
    });

    ctx.redirect(&apos;/admin/auth/login?payload=&apos; + payload);
    return
  }
};</code></pre><p>In the admin dashboard, we have to set the API to public. and generate a <code>full-access</code> token for later use. Therefore, when we access <code>$URL/api/teleport?token=${token}</code>, it will redirect us to the admin login page, with the payload containing user data and token as the query param.</p><h2 id="part-2-login-the-user-with-the-credential-previously-obtained">Part 2: Login the user with the credential previously obtained </h2><p>Now we need to customize the login page logic, to check if we have <code>payload</code> as a parameter inside the url. However, unlike v3 version, in Strapi v4, we need to do the following works to customize:</p><p>First, we need the <code>patch-package</code> package as a dependency.</p><pre><code class="language-bash">yarn install patch-package</code></pre><p>Edit<code>node_modules/@strapi/admin/admin/src/pages/AuthPage/index.js</code> after installing the package. Add the following lines below the &quot;Redirect the user to the login page if the endpoint does not exist or there is already an admin user or the user is already logged in&quot; code block.</p><pre><code class="language-javascript">  // if url param has payload
  if (search) {
    const params = new URLSearchParams(search);
    const payload = params.get(&apos;payload&apos;);

    if (payload) { 
      const { user, jwtToken } = JSON.parse(payload)

      auth.setToken(jwtToken, false);
      auth.setUserInfo(user, false);

      return &lt;Redirect to=&quot;/&quot; /&gt;;
    }

    else {
      return &lt;Redirect to=&quot;/auth/login&quot; /&gt;;
    }
  }</code></pre><p>A patch file will be generated inside the <code>patches</code> folder, and run <code>yarn build</code> to rebuild the admin dashboard. Open <code>$url/api/teleport?token=${token}</code> again and you should notice it will redirect to the admin dashboard and log in the admin user.</p><h2 id="part-3-dont-forget-to-apply-the-patch">Part 3: Don&apos;t forget to apply the patch!</h2><p>When you are going to deploy Strapi to the server, remember to run the <code>yarn patch-package</code> command to apply the patch before rebuilding the resources.</p>]]></content:encoded></item><item><title><![CDATA[How to merge git commits from other remote origin]]></title><description><![CDATA[<ol><li>Add the remote</li></ol><pre><code>git remote add &lt;new_remote&gt; &lt;url&gt;</code></pre><ol start="2"><li>Create a new branch tracking that remote</li></ol><pre><code>git checkout -b &lt;new_branch&gt; --track &lt;new_remote&gt;/main</code></pre><ol start="3"><li>Cherry-pick the commits you want</li></ol><pre><code>git cherry-pick &lt;commit_hash_old&gt;^..&lt;commit_hash_new&gt;</code></pre>]]></description><link>https://sophiecao.me/how-to-merge-git-commits-from-other-remote-origin/</link><guid isPermaLink="false">65c2e264e05771000106c19b</guid><category><![CDATA[Git]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Wed, 07 Feb 2024 02:05:11 GMT</pubDate><content:encoded><![CDATA[<ol><li>Add the remote</li></ol><pre><code>git remote add &lt;new_remote&gt; &lt;url&gt;</code></pre><ol start="2"><li>Create a new branch tracking that remote</li></ol><pre><code>git checkout -b &lt;new_branch&gt; --track &lt;new_remote&gt;/main</code></pre><ol start="3"><li>Cherry-pick the commits you want</li></ol><pre><code>git cherry-pick &lt;commit_hash_old&gt;^..&lt;commit_hash_new&gt;</code></pre><ol start="4"><li>Resolve any conflicts and continue</li></ol><pre><code>git cherry-pick --continue</code></pre><p>All done!</p>]]></content:encoded></item><item><title><![CDATA[Fix Android ListViewItem not highlighted when selecting programmatically]]></title><description><![CDATA[<figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://sophiecao.me/content/images/2024/01/before-1.png" width="1440" height="3120" loading="lazy" alt srcset="https://sophiecao.me/content/images/size/w600/2024/01/before-1.png 600w, https://sophiecao.me/content/images/size/w1000/2024/01/before-1.png 1000w, https://sophiecao.me/content/images/2024/01/before-1.png 1440w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://sophiecao.me/content/images/2024/01/after-1.png" width="1440" height="3120" loading="lazy" alt srcset="https://sophiecao.me/content/images/size/w600/2024/01/after-1.png 600w, https://sophiecao.me/content/images/size/w1000/2024/01/after-1.png 1000w, https://sophiecao.me/content/images/2024/01/after-1.png 1440w" sizes="(min-width: 720px) 720px"></div></div></div></figure><p>I have a <code>ListView</code> like this:</p><pre><code class="language-xml">&lt;ListView
    android:id=&quot;@+id/listView&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:background=&quot;@color/white&quot;
    android:choiceMode=&quot;singleChoice&quot;
    android:listSelector=&quot;?attr/colorPrimarySurface&quot; /&gt;</code></pre><p>However, when I tried</p>]]></description><link>https://sophiecao.me/fix-android-list-view-item-not-highlighted-when-selecting-programmatically/</link><guid isPermaLink="false">65b14dfd99b223000110f345</guid><category><![CDATA[Android]]></category><category><![CDATA[Kotlin]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Wed, 24 Jan 2024 17:59:10 GMT</pubDate><media:content url="https://sophiecao.me/content/images/2024/02/after-1-1.png" medium="image"/><content:encoded><![CDATA[<figure class="kg-card kg-gallery-card kg-width-wide"><div class="kg-gallery-container"><div class="kg-gallery-row"><div class="kg-gallery-image"><img src="https://sophiecao.me/content/images/2024/01/before-1.png" width="1440" height="3120" loading="lazy" alt="Fix Android ListViewItem not highlighted when selecting programmatically" srcset="https://sophiecao.me/content/images/size/w600/2024/01/before-1.png 600w, https://sophiecao.me/content/images/size/w1000/2024/01/before-1.png 1000w, https://sophiecao.me/content/images/2024/01/before-1.png 1440w" sizes="(min-width: 720px) 720px"></div><div class="kg-gallery-image"><img src="https://sophiecao.me/content/images/2024/01/after-1.png" width="1440" height="3120" loading="lazy" alt="Fix Android ListViewItem not highlighted when selecting programmatically" srcset="https://sophiecao.me/content/images/size/w600/2024/01/after-1.png 600w, https://sophiecao.me/content/images/size/w1000/2024/01/after-1.png 1000w, https://sophiecao.me/content/images/2024/01/after-1.png 1440w" sizes="(min-width: 720px) 720px"></div></div></div></figure><img src="https://sophiecao.me/content/images/2024/02/after-1-1.png" alt="Fix Android ListViewItem not highlighted when selecting programmatically"><p>I have a <code>ListView</code> like this:</p><pre><code class="language-xml">&lt;ListView
    android:id=&quot;@+id/listView&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    android:background=&quot;@color/white&quot;
    android:choiceMode=&quot;singleChoice&quot;
    android:listSelector=&quot;?attr/colorPrimarySurface&quot; /&gt;</code></pre><p>However, when I tried to highlight on the first item by default using the <code>performItemClick</code> method, it won&apos;t work.</p><pre><code class="language-kotlin">listView.post {
    listView.performItemClick(
        listView.getChildAt(0),
        0,
        listView.adapter.getItemId(0)
    )
}</code></pre><p>After searching on the internet, i found the solution, two lines needed to be added before calling the <code>performItemClick</code> method.</p><pre><code class="language-kotlin">listView.requestFocusFromTouch();
listView.setSelection(0);</code></pre><p>Now it works as expected.</p><p>ref: <a href="https://stackoverflow.com/a/34566428?ref=sophiecao.me">https://stackoverflow.com/a/34566428</a></p>]]></content:encoded></item><item><title><![CDATA[An experiment on Swift references]]></title><description><![CDATA[<p>In Swift, we have 4 different kinds of references, so I did a little experiment to help me to have a better understanding of them. There are many articles already explained the difference between these different kinds and their use case respectively, in a detailed and understandable manner, therefore in</p>]]></description><link>https://sophiecao.me/an-experiment-on-swift-references/</link><guid isPermaLink="false">6543c81d99b223000110f2ad</guid><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Thu, 02 Nov 2023 16:20:21 GMT</pubDate><content:encoded><![CDATA[<p>In Swift, we have 4 different kinds of references, so I did a little experiment to help me to have a better understanding of them. There are many articles already explained the difference between these different kinds and their use case respectively, in a detailed and understandable manner, therefore in this post I&apos;ll focused on what will actually happened if you deallocate them.</p><h2 id="setting-up">Setting up</h2><p>First we have this piece of code</p><pre><code class="language-swift">class MagicBox {
    
    var strongObj: AnyObject?
    
    weak var weakObj: AnyObject?
    
    /// this is equivalent to unowned(safe)
    /// don&apos;t make this optional in real project
    unowned var unownedObject: AnyObject?
    
    unowned(unsafe) var unownedUnsafeObject: AnyObject?
    
    init(strongObj: AnyObject, weakObj: AnyObject, unownedObj: AnyObject, unownedUnsafeObj: AnyObject) {
        self.strongObj = strongObj
        self.weakObj = weakObj
        self.unownedObject = unownedObj
        self.unownedUnsafeObject = unownedUnsafeObj
    }
    
    func all() -&gt; String {
        // workaround so to access these variables first
        if (self.unownedUnsafeObject == nil) {}    
        if (self.unownedObject == nil) {}
        
        return &quot;strongObj: \(strongObj)\nweakObj: \(weakObj)\nunownedObject: \(unownedObject)\nunownedUnsafeObject: \(unownedUnsafeObject)&quot;
    }
}</code></pre><p>Then we create some objects and referenced them in our <code>MagicBox</code></p><pre><code class="language-swift">var strongObj: AnyObject? = NSObject()
var weakObj: AnyObject? = NSObject()
var unownedObj: AnyObject? = NSObject()
var unownedUnsafeObj: AnyObject? = NSObject()

var magicBox = MagicBox(strongObj: strongObj!, weakObj: weakObj!, unownedObj: unownedObj!, unownedUnsafeObj: unownedUnsafeObj!)
magicBox.all()</code></pre><p>Here is the output:</p><pre><code>strongObj: Optional(&lt;NSObject: 0x600002cfcb80&gt;)
weakObj: Optional(&lt;NSObject: 0x600002cfcba0&gt;)
unownedObject: Optional(&lt;NSObject: 0x600002cfcc10&gt;)
unownedUnsafeObject: Optional(&lt;NSObject: 0x600002cfcc20&gt;)</code></pre><p>Now we try to set the value of <code>strongObj</code> and <code>weakObj</code> to nil, and here is what happened, the <code>weakObj</code> is vanished but the <code>strongObj</code> is still there.</p><pre><code>strongObj: Optional(&lt;NSObject: 0x600002c08cd0&gt;)
weakObj: nil
unownedObject: Optional(&lt;NSObject: 0x600002c08d60&gt;)
unownedUnsafeObject: Optional(&lt;NSObject: 0x600002c08d70&gt;)</code></pre><p>Next we are going to set <code>unownedUnsafeObject</code> to nil, and we got an error</p><pre><code>error: Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x614744894bc0).
The process has been left at the point where it was interrupted, use &quot;thread return -x&quot; to return to the state before expression evaluation.</code></pre><p>Finally is the <code>unownedObject</code>, and we got a different error</p><pre><code>error: Execution was interrupted, reason: signal SIGABRT.
The process has been left at the point where it was interrupted, use &quot;thread return -x&quot; to return to the state before expression evaluation.</code></pre>]]></content:encoded></item><item><title><![CDATA[Improving Language Study Observability: Number Familiarity Evaluation using the ELK Stack]]></title><description><![CDATA[<h2 id="introduction">Introduction</h2><p>As non-native language speakers, we often have difficulties quickly responding to numbers spoken using foreign languages. To overcome this issue, we have to practice more often. However, this is a long process and we might lose confidence and patience. Is there a way to let us understand more about</p>]]></description><link>https://sophiecao.me/improving-language-study-observability-number-familiarity-evaluation-using-the-elk-stack/</link><guid isPermaLink="false">6535867599b223000110f261</guid><category><![CDATA[ELK]]></category><category><![CDATA[Data Visualisation]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Frontend]]></category><category><![CDATA[IELTS]]></category><category><![CDATA[TEF]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Sun, 22 Oct 2023 20:37:22 GMT</pubDate><media:content url="https://sophiecao.me/content/images/2023/10/NUmeric-2.png" medium="image"/><content:encoded><![CDATA[<h2 id="introduction">Introduction</h2><img src="https://sophiecao.me/content/images/2023/10/NUmeric-2.png" alt="Improving Language Study Observability: Number Familiarity Evaluation using the ELK Stack"><p>As non-native language speakers, we often have difficulties quickly responding to numbers spoken using foreign languages. To overcome this issue, we have to practice more often. However, this is a long process and we might lose confidence and patience. Is there a way to let us understand more about our ability through visualization and gamification? Probably there is one.</p><h2 id="whats-in-the-market"><strong>What&apos;s in the market</strong></h2><p>There are many apps in the market that help users to be familiar with foreign languages through dictation. Once the application pronounces the number, the user will have to answer using Arabic numerals. However, none of them provide statistical features for users to get insights.</p><h2 id="thoughts"><strong>Thoughts</strong></h2><p>How about we keep a record of the user&apos;s response? Which can be used for further studies? What should be recorded?</p><ul><li>language</li><li>how many digits</li><li>the number spoken</li><li>the user&apos;s response time</li><li>whether the user answered correctly</li></ul><p>And what could be visualized</p><ul><li>trend of user&apos;s response time</li><li>trend of user&apos;s accuracy</li><li>user&apos;s average response time</li><li>frequent mistakes the user made</li></ul><p>Sounds good, so where to start?</p><h2 id="introducing-the-elk-stack"><strong>Introducing the ELK stack</strong></h2><p>ELK (Elasticsearch, Logstash, Kibana) is a popular software stack for logging and analysis. Therefore, we can utilize the ELK stack to achieve this.</p><p>The user will make the answer on our front-end application. After the user answers, the metrics will be sent to Logstash via HTTP request. Logstash will process the data and save them to Elasticsearch. Then, we can use Kibana to do the visualization.</p><h2 id="implementation"><strong>Implementation</strong></h2><h3 id="front-end"><strong>Front-end</strong></h3><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sophiecao.me/content/images/2023/10/Capture-d--cran--le-2023-10-22---16.30.13.png" class="kg-image" alt="Improving Language Study Observability: Number Familiarity Evaluation using the ELK Stack" loading="lazy" width="2000" height="1250" srcset="https://sophiecao.me/content/images/size/w600/2023/10/Capture-d--cran--le-2023-10-22---16.30.13.png 600w, https://sophiecao.me/content/images/size/w1000/2023/10/Capture-d--cran--le-2023-10-22---16.30.13.png 1000w, https://sophiecao.me/content/images/size/w1600/2023/10/Capture-d--cran--le-2023-10-22---16.30.13.png 1600w, https://sophiecao.me/content/images/size/w2400/2023/10/Capture-d--cran--le-2023-10-22---16.30.13.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Screenshot of the front-end</figcaption></figure><p>The front end is rather simple, which utilizes the <a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis?ref=sophiecao.me">SpeechSynthetis</a> API.</p><pre><code class="language-javascript">const speak = () =&gt; {
  var synthesis = window.speechSynthesis;

  // Get the first voice in the list
  var voice = synthesis.getVoices().filter(function (voice) {
    return voice.lang === language.value;
  })[0];

  let number = currentNumber.value;

  if (pronunceSeperator.value) {
    number = addThousandSeperator(number);
  }

  // Create an utterance object
  var utterance = new SpeechSynthesisUtterance(number);

  // Set utterance properties
  utterance.voice = voice;
  utterance.pitch = 1.5;
  utterance.rate = 1.25;
  utterance.volume = 1;

  // Speak the utterance
  synthesis.speak(utterance);
  utterance.onend = startTiming;
};</code></pre><p>One caveat discovered is when pronouncing four-digit numbers using <code>en-*</code> languages, it will pronounced by each two digits. The workaround is to add number separators:</p><pre><code class="language-javascript">const addThousandSeperator = (number) =&gt; {
  return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, &quot;,&quot;);
};</code></pre><p>To get precise time, the <a href="https://developer.mozilla.org/en-US/docs/Web/API/Performance/now?ref=sophiecao.me">performance.now()</a> will be used. Once the utterance end, it will get the current performance timing, and once user entered, the delta between two time will be calculated to get the user&apos;s response time.</p><pre><code class="language-javascript">stats.performanceMs = BigInt(
  (performance.now() - stats.startPerformanceMs).toFixed(0)
);</code></pre><p>The data sent to Logstash is constructed using the following:</p><pre><code class="language-javascript">record.value.push({
  lang: language.value,
  number: currentNumber.value,
  time: stats.performanceMs,
  isCorrect: currentNumberInput.value == currentNumber.value,
  date: new Date().toISOString().slice(0, 10),
  digits: currentNumber.value.length,
});</code></pre><h3 id="elk"><strong>ELK</strong></h3><p>For the ELK side, we use <a href="https://github.com/deviantony/docker-elk?ref=sophiecao.me">docker-elk</a> to set up the ELK stack. After setting up, we will add a pipeline in the <code>logstash/pipeline</code> directory.</p><pre><code class="language-configure">input {
  http {
    port =&gt; 8898

    response_headers =&gt; {
      &quot;Access-Control-Allow-Origin&quot; =&gt; &quot;*&quot;
      &quot;Content-Type&quot; =&gt; &quot;text/plain&quot;
      &quot;Access-Control-Allow-Headers&quot; =&gt; &quot;Origin, X-Requested-With, Content-Type, Accept&quot;
    }
  }
}

filter {
  split {
    field =&gt; &quot;data&quot;
  }

  mutate {
    convert =&gt; [&quot;time&quot;, &quot;integer&quot;]
  }

  mutate {
    convert =&gt; [&quot;number&quot;, &quot;integer&quot;]
  }
}

output {
  elasticsearch {
    hosts =&gt; &quot;elasticsearch:9200&quot;
    user =&gt; &quot;logstash_internal&quot;
    password =&gt; &quot;changeme&quot;
    index =&gt; &quot;numeric&quot;
  }
}</code></pre><p>Then, we have to configure the permission and role inside Kibana. After that, Logstash is able to collect the metrics for elastic search. And it is time for us to create visualization. Below is the metrics that I found useful. The metrics for using native language served as a baseline for comparison.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://sophiecao.me/content/images/2023/10/NUmeric-2-1.png" class="kg-image" alt="Improving Language Study Observability: Number Familiarity Evaluation using the ELK Stack" loading="lazy" width="2000" height="1423" srcset="https://sophiecao.me/content/images/size/w600/2023/10/NUmeric-2-1.png 600w, https://sophiecao.me/content/images/size/w1000/2023/10/NUmeric-2-1.png 1000w, https://sophiecao.me/content/images/size/w1600/2023/10/NUmeric-2-1.png 1600w, https://sophiecao.me/content/images/size/w2400/2023/10/NUmeric-2-1.png 2400w" sizes="(min-width: 720px) 720px"><figcaption>Screenshot of the visualization</figcaption></figure><p>For the number error rate, the TSVB should be used to allow sorting using the error rate, which is derived from custom query.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/dodaydream/number-training-elk?ref=sophiecao.me"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - dodaydream/number-training-elk</div><div class="kg-bookmark-description">Contribute to dodaydream/number-training-elk development by creating an account on GitHub.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.com/fluidicon.png" alt="Improving Language Study Observability: Number Familiarity Evaluation using the ELK Stack"><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">dodaydream</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/e3f49da38b7a4f051ccf41c119e7dff29bdd8cf591d9b7f1fe016a794c0b0d07/dodaydream/number-training-elk" alt="Improving Language Study Observability: Number Familiarity Evaluation using the ELK Stack"></div></a></figure>]]></content:encoded></item><item><title><![CDATA[Configure circuit break fallback for OpenFeign]]></title><description><![CDATA[<p>Lately I&#x2019;ve been experimenting with microservices by following a tutorial. I have managed to set up Spring Cloud with Eureka and Feign successfully. However, I&apos;ve encountered some difficulties.</p><p>In the tutorial, it provides an example using Eureka, Feign and Hystrix. However, it is using an older</p>]]></description><link>https://sophiecao.me/configure-circuit-break-fallback-for-open-feign/</link><guid isPermaLink="false">6503940a99b223000110f1f7</guid><category><![CDATA[Backend]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Thu, 14 Sep 2023 23:35:34 GMT</pubDate><content:encoded><![CDATA[<p>Lately I&#x2019;ve been experimenting with microservices by following a tutorial. I have managed to set up Spring Cloud with Eureka and Feign successfully. However, I&apos;ve encountered some difficulties.</p><p>In the tutorial, it provides an example using Eureka, Feign and Hystrix. However, it is using an older version of Spring Boot, while I&apos;m using the latested version. After checking on the <a href="https://github.com/Netflix/Hystrix?ref=sophiecao.me">project page</a> of Hystrix, I found out it was in maintanence mode. Moreover, it seems that <a href="https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/?ref=sophiecao.me#spring-cloud-feign-circuitbreaker">Spring Cloud OpenFeign</a> has also dropped support of Hystrix.</p><p>Therefore I decided to switch to another circuit breaker since this was a easier way. </p><p>Firstly, circuit breaker should be enabled</p><pre><code class="language-yaml">spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true</code></pre><p>Then make sure to add these annotations to the application class</p><pre><code class="language-java">@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication</code></pre><p>The client class and fallback should be like this:</p><pre><code class="language-java">@FeignClient(name = &quot;microservice-provider-user&quot;, fallback = UserFeignClientFallback.class)
public interface UserFeignClient {

    @RequestMapping(&quot;/{id}&quot;)
    User findById(@PathVariable(&quot;id&quot;) Long id);

}</code></pre><pre><code class="language-java">@Component
class UserFeignClientFallback implements UserFeignClient {

    @Override
    public User findById(Long id) {
        User user = new User();
        user.setId(-1L);
        user.setName(&quot;user&quot;);
        return user;
    }
}</code></pre><p>Error may poped up when using the IDE, but the code will compile, for further discussion, please <a href="https://github.com/spring-cloud/spring-cloud-netflix/issues/899?ref=sophiecao.me">see this issue</a>. Alternative might be nested the fallback class into the interface.</p><pre><code class="language-java">@Autowired
MovieController(UserFeignClient userFeignClient) {
    this.userFeignClient = userFeignClient;
}</code></pre>]]></content:encoded></item><item><title><![CDATA[Understanding Database Normalization in ORM Language]]></title><description><![CDATA[<h2 id="1nf">1NF</h2><p>One column could only contain one type of data (i.e. no array, no object)</p><p>Example: consider we have a table called <code>users</code>, which has one column <code>telephones</code> with data like this <code>(111) 222-3333, (444) 555-6666</code>, then it violates 1nf.</p><p>The approach to making the table to be compliant</p>]]></description><link>https://sophiecao.me/understanding-database-normalization-in-orm-language/</link><guid isPermaLink="false">646c369099b223000110f130</guid><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Tue, 23 May 2023 05:04:26 GMT</pubDate><content:encoded><![CDATA[<h2 id="1nf">1NF</h2><p>One column could only contain one type of data (i.e. no array, no object)</p><p>Example: consider we have a table called <code>users</code>, which has one column <code>telephones</code> with data like this <code>(111) 222-3333, (444) 555-6666</code>, then it violates 1nf.</p><p>The approach to making the table to be compliant with 1NF is to save the telephone numbers into two columns, <code>telephone1</code> and <code>telephone2</code>.</p><h2 id="2nf">2NF</h2><p>Each attribute of the object should only depend on the primary key. If the uniqueness of the object depends on other attributes, then it may be good to split the class into two or more classes.</p><p>Example: we now have a table called <code>enrollments</code>, which has the following fields: <code>student_id</code>, <code>course_id</code>, <code>course_name</code>, <code>professor</code>. Then it violates the 2nf.</p><p>Therefore, we have to extract the course info out of the table, now we have two tables, <code>enrollments</code> and <code>courses</code>.</p><pre><code>Dependencies in the enrollments table:
(student_id, course_id) -&gt; course_name

course_id -&gt; course_name</code></pre><h2 id="3nf">3NF</h2><p>There should be no transitive dependency on the non-primary attributes. If one attribute could be decided based on other attributes, it should be extracted to a new class.</p><p>Still considering the previous example, if the table <code>courses</code> has a column called <code>office</code>, then it violates the 3nf. because the attribute <code>office</code> depends on the column <code>professor</code>, not the <code>course</code>.</p><pre><code>Dependencies in the course table:

course_id -&gt; professor -&gt; office
course_id -&gt; office</code></pre><h2 id="bcnf">BCNF</h2><p>3NF addressed that the non-primary key attributes cannot have a transitive dependency, while BCNF further stated that the attribute should also not be dependent on the primary key. This usually occurs if there are multiple primary keys in the table.</p><pre><code>ISBN, NAME, AUTHOR

ISBN -&gt; NAME
(NAME, AUTHOR) -&gt; ISBN

(NAME, AUTHOR) -&gt; ISBN -&gt; NAME


(NAME, AUTHOR) -&gt; NAME</code></pre><h2 id="4nf">4NF</h2><p>In a many-to-many relationship, if the attributes in the pivot table are not related, then it violates the 4nf relationship and should be separated into multiple many-to-many relationships.</p><h2 id="5nf">5NF</h2><p>Based on 4NF, Even if the attributes in the pivot table are related, they should be further separated.</p><p></p>]]></content:encoded></item><item><title><![CDATA[主观构造的世界]]></title><description><![CDATA[<p>&#x8FD9;&#x7BC7;&#x6587;&#x7AE0;&#x662F;&#x300A;&#x5BFB;&#x627E;&#x6700;&#x4F73;&#x5B9E;&#x8DF5;&#x300B;&#x7CFB;&#x5217;&#x7684;&#x7B2C;&#x4E8C;&#x7BC7;&#x6587;&#x7AE0;&#x3002;&#x53EF;&#x80FD;&#x770B;&#x8D77;&#x6765;&#x6709;&#x4E00;&#x79CD;&#x6CA1;&#x6D3B;&#x786C;&#x6574;&#x6D3B;&#x7684;&#x611F;&#x89C9;&#xFF0C;&#x4F46;&#x8C01;&#x8BA9;&#x4EBA;&#x751F;&#x7684;&#x610F;&#x4E49;&#x5C31;&#x662F;&#x4E0D;</p>]]></description><link>https://sophiecao.me/subjectively-constructed-world/</link><guid isPermaLink="false">6466d5ff99b223000110f08d</guid><category><![CDATA[Miscellaneous]]></category><category><![CDATA[Philosophy]]></category><category><![CDATA[寻找最佳实践]]></category><category><![CDATA[Finding best practices]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Fri, 19 May 2023 02:50:08 GMT</pubDate><content:encoded><![CDATA[<p>&#x8FD9;&#x7BC7;&#x6587;&#x7AE0;&#x662F;&#x300A;&#x5BFB;&#x627E;&#x6700;&#x4F73;&#x5B9E;&#x8DF5;&#x300B;&#x7CFB;&#x5217;&#x7684;&#x7B2C;&#x4E8C;&#x7BC7;&#x6587;&#x7AE0;&#x3002;&#x53EF;&#x80FD;&#x770B;&#x8D77;&#x6765;&#x6709;&#x4E00;&#x79CD;&#x6CA1;&#x6D3B;&#x786C;&#x6574;&#x6D3B;&#x7684;&#x611F;&#x89C9;&#xFF0C;&#x4F46;&#x8C01;&#x8BA9;&#x4EBA;&#x751F;&#x7684;&#x610F;&#x4E49;&#x5C31;&#x662F;&#x4E0D;&#x65AD;&#x6574;&#x6D3B;&#x5462;&#xFF1F;&#x65E0;&#x8BBA;&#x662F;&#x597D;&#x6D3B;&#x8FD8;&#x662F;&#x70C2;&#x6D3B;&#xFF0C;&#x90FD;&#x662F;&#x4EBA;&#x7C7B;&#x521B;&#x9020;&#x548C;&#x63A2;&#x9669;&#x7CBE;&#x795E;&#x7684;&#x4F53;&#x73B0;&#x3002;</p><p></p><p>&#x5F77;&#x82E5;&#x88AB;&#x96F7;&#x51FB;&#x4E00;&#x822C;&#xFF0C;&#x601D;&#x7EEA;&#x88AB;&#x62C9;&#x5165;&#x4E86;&#x65F6;&#x7A7A;&#x6F29;&#x6DA1;&#x4E4B;&#x4E2D;&#xFF1B;&#x539F;&#x672C;&#x575A;&#x5B9A;&#x7684;&#x4FE1;&#x5FF5;&#x5F00;&#x59CB;&#x88AB;&#x6000;&#x7591;&#x5360;&#x636E;&#x3002;&#x8111;&#x6D77;&#x91CC;&#x5F00;&#x59CB;&#x8D28;&#x95EE;&#x81EA;&#x5DF1;&#xFF0C;&#x8D28;&#x95EE; ta &#x4EBA;&#x3002;&#x5374;&#x552F;&#x72EC;&#x5FD8;&#x8BB0;&#x8D28;&#x95EE;&#x81EA;&#x5DF1;&#x7684;&#x60C5;&#x7EEA;&#x3002;</p><p>&#x76F8;&#x4FE1;&#x6BCF;&#x4E2A;&#x4EBA;&#x90FD;&#x66FE;&#x7ECF;&#x6709;&#x8FC7;&#x8FD9;&#x6837;&#x7684;&#x56DE;&#x5FC6;&#x3002;&#x5728;&#x67D0;&#x4E00;&#x523B;&#xFF0C;&#x7A81;&#x7136;&#x9677;&#x5165;&#x7EF5;&#x5EF6;&#x7684;&#x60C5;&#x7EEA;&#x4E4B;&#x4E2D;&#xFF0C;&#x5F00;&#x59CB;&#x6000;&#x7591;&#x81EA;&#x5DF1;&#x6BCF;&#x4E00;&#x4E2A;&#x9009;&#x62E9;&#x7684;&#x610F;&#x4E49;&#x3002;&#x800C;&#x81EA;&#x6211;&#x4FDD;&#x62A4;&#x4E5F;&#x968F;&#x4E4B;&#x88AB;&#x89E6;&#x53D1;&#x3002;&#x6211;&#x4EEC;&#x4E5F;&#x987A;&#x5229;&#x6210;&#x7AE0;&#x7684;&#x5FFD;&#x7565;&#x4E86;&#x6000;&#x7591;&#x8FD9;&#x6000;&#x7591;&#x672C;&#x8EAB;&#x3002;&#x601D;&#x7EEA;&#x8FDB;&#x5165;&#x4E86;&#x8111;&#x8865;&#x6A21;&#x5F0F;&#xFF0C;&#x8F93;&#x51FA;&#x7740;&#x90A3;&#x4E9B;&#x770B;&#x4F3C;&#x5408;&#x7406;&#x4F46;&#x4ECE;&#x672A;&#x53D1;&#x751F;&#x7684;&#x6240;&#x8C13;&#x4E8B;&#x5B9E;&#x3002;</p><p>&#x66F4;&#x7CDF;&#x7CD5;&#x7684;&#x662F;&#xFF0C;&#x8FD9;&#x601D;&#x7EEA;&#x632F;&#x632F;&#x6709;&#x8BCD;&#xFF0C;&#x81EA;&#x4FE1;&#x6EE1;&#x6EE1;&#x3002;&#x800C;&#x6211;&#x4EEC;&#x9762;&#x5BF9;&#x7740;&#x8FD9;&#x4E9B;&#x300C;&#x4E8B;&#x5B9E;&#x300D;&#xFF0C;&#x4E0D;&#x514D;&#x4E5F;&#x5C11;&#x4E86;&#x51E0;&#x5206;&#x81EA;&#x4FE1;&#x548C;&#x5224;&#x65AD;&#x529B;&#xFF0C;&#x4E45;&#x800C;&#x4E45;&#x4E4B;&#xFF0C;&#x8FD9;&#x4E9B;&#x300C;&#x4E8B;&#x5B9E;&#x300D;&#x4FBF;&#x6210;&#x4E3A;&#x4E86;&#x6211;&#x4EEC;&#x8111;&#x6D77;&#x4E2D;&#x771F;&#x5B9E;&#x5B58;&#x5728;&#x7684;&#x8BB0;&#x5FC6;&#x3002;&#x53D8;&#x6210;&#x6211;&#x4EEC;&#x4ECA;&#x540E;&#x601D;&#x8003;&#x7684;&#x4F9D;&#x636E;&#x4E4B;&#x4E00;&#x3002;&#x800C;&#x6210;&#x89C1;&#x4E5F;&#x4ECE;&#x6B64;&#x5F00;&#x59CB;&#x52A0;&#x6DF1;&#x3002;</p><p>&#x6B63;&#x5982;&#x5EB7;&#x5FB7;&#x6240;&#x8A00;&#xFF0C;&#x4EBA;&#x6C38;&#x8FDC;&#x65E0;&#x6CD5;&#x5BF9;&#x672C;&#x4F53;&#x5177;&#x6709;&#x76F4;&#x63A5;&#x7684;&#x8BA4;&#x8BC6;&#xFF0C;&#x53EA;&#x80FD;&#x901A;&#x8FC7;&#x611F;&#x77E5;&#x548C;&#x7406;&#x89E3;&#x6765;&#x5C1D;&#x8BD5;&#x8BA4;&#x8BC6;&#x672C;&#x4F53;&#x3002;&#x8FD9;&#x79CD;&#x8BA4;&#x8BC6;&#x662F;&#x4E3B;&#x89C2;&#x7684;&#xFF0C;&#x56E0;&#x4EBA;&#x800C;&#x5F02;&#x7684;&#x3002;&#x4F46;&#x56E0;&#x4E3A;&#x4EBA;&#x7C7B;&#x601D;&#x7EF4;&#x7684;&#x5171;&#x6027;&#xFF0C;&#x6240;&#x4EE5;&#x5728;&#x67D0;&#x79CD;&#x7A0B;&#x5EA6;&#x4E0A;&#xFF0C;&#x4EBA;&#x7C7B;&#x5206;&#x4EAB;&#x7740;&#x76F8;&#x540C;&#x7684;&#x8BA4;&#x77E5;&#x3002;</p><p>&#x800C;&#x9762;&#x5BF9;&#x524D;&#x8FF0;&#x7684;&#x601D;&#x7EEA;&#x6F29;&#x6DA1;&#xFF0C;&#x6709;&#x7684;&#x4EBA;&#x9009;&#x62E9;&#x4E86;&#x6C89;&#x9ED8;&#x548C;&#x81EA;&#x6211;&#x6D88;&#x5316;&#xFF0C;ta &#x4EEC;&#x610F;&#x8BC6;&#x5230;&#xFF0C;&#x5982;&#x679C;&#x5C06;&#x8FD9;&#x4E9B;&#x60C5;&#x7EEA;&#x4F5C;&#x4E3A;&#x884C;&#x4E8B;&#x7684;&#x4F9D;&#x636E;&#xFF0C;&#x672A;&#x514D;&#x592A;&#x8FC7;&#x6B66;&#x65AD;&#x3002;&#x800C;&#x4E14;&#xFF0C;&#x5982;&#x679C;&#x4E0D;&#x52A0;&#x533A;&#x5206;&#x5730;&#x5C06;&#x6240;&#x6709;&#x60C5;&#x7EEA;&#x90FD;&#x5F53;&#x4F5C;&#x771F;&#x5B9E;&#x7684;&#x300C;&#x4E8B;&#x5B9E;&#x300D;&#xFF0C;&#x53EF;&#x80FD;&#x4F1A;&#x88AB; ta &#x4EBA;&#x89C6;&#x4E3A;&#x83AB;&#x540D;&#x5176;&#x5999;&#xFF0C;&#x751A;&#x81F3;&#x53EF;&#x80FD;&#x9020;&#x6210;&#x65E0;&#x6CD5;&#x627F;&#x62C5;&#x7684;&#x540E;&#x679C;&#x3002;</p><p>&#x8BA9;&#x6211;&#x4EEC;&#x770B;&#x4E00;&#x4E9B;&#x4F8B;&#x5B50;&#xFF1A;</p><ol><li>&#x5728;&#x804C;&#x573A;&#x4E0A;&#xFF0C;&#x5047;&#x8BBE;&#x6709;&#x4E2A;&#x5458;&#x5DE5;&#x7ECF;&#x5E38;&#x62B1;&#x6028;&#x4ED6;&#x7684;&#x4E3B;&#x7BA1;&#x603B;&#x662F;&#x504F;&#x5411;&#x5176;&#x4ED6;&#x4EBA;&#x3002;&#x4ED6;&#x53EF;&#x80FD;&#x4F1A;&#x5C06;&#x8FD9;&#x79CD;&#x60C5;&#x51B5;&#x89C6;&#x4E3A;&#x4E8B;&#x5B9E;&#xFF0C;&#x611F;&#x89C9;&#x81EA;&#x5DF1;&#x53D7;&#x5230;&#x4E86;&#x4E0D;&#x516C;&#x5E73;&#x7684;&#x5F85;&#x9047;&#x3002;&#x7136;&#x800C;&#xFF0C;&#x5982;&#x679C;&#x4ED6;&#x51B3;&#x5B9A;&#x6682;&#x505C;&#x4E00;&#x4E0B;&#xFF0C;&#x53CD;&#x601D;&#x4ED6;&#x7684;&#x60C5;&#x7EEA;&#xFF0C;&#x4ED6;&#x53EF;&#x80FD;&#x4F1A;&#x53D1;&#x73B0;&#x4ED6;&#x7684;&#x62B1;&#x6028;&#x66F4;&#x591A;&#x7684;&#x662F;&#x6E90;&#x4E8E;&#x4ED6;&#x81EA;&#x5DF1;&#x7684;&#x4E0D;&#x5B89;&#x5168;&#x611F;&#x6216;&#x8005;&#x5BF9;&#x627F;&#x8BA4;&#x81EA;&#x8EAB;&#x4E0D;&#x8DB3;&#x7684;&#x6050;&#x60E7;&#x3002;&#x4E00;&#x65E6;&#x4ED6;&#x8BA4;&#x8BC6;&#x5230;&#x8FD9;&#x4E00;&#x70B9;&#xFF0C;&#x4ED6;&#x5C31;&#x80FD;&#x5F00;&#x59CB;&#x63A2;&#x7D22;&#x5982;&#x4F55;&#x63D0;&#x5347;&#x81EA;&#x5DF1;&#x7684;&#x6280;&#x80FD;&#xFF0C;&#x800C;&#x4E0D;&#x662F;&#x9677;&#x5165;&#x62B1;&#x6028;&#x4E2D;&#x3002;</li><li>&#x4E00;&#x4E2A;&#x5B66;&#x751F;&#x53EF;&#x80FD;&#x4F1A;&#x62B1;&#x6028;&#x4ED6;&#x7684;&#x6570;&#x5B66;&#x8001;&#x5E08;&#x5BF9;&#x4ED6;&#x7684;&#x671F;&#x671B;&#x592A;&#x9AD8;&#x3002;&#x4ED6;&#x53EF;&#x80FD;&#x4F1A;&#x89C9;&#x5F97;&#x8FD9;&#x6837;&#x7684;&#x671F;&#x671B;&#x4E0D;&#x516C;&#x5E73;&#xFF0C;&#x751A;&#x81F3;&#x8BA4;&#x4E3A;&#x81EA;&#x5DF1;&#x65E0;&#x6CD5;&#x8FBE;&#x5230;&#x3002;&#x7136;&#x800C;&#xFF0C;&#x5982;&#x679C;&#x4ED6;&#x80FD;&#x591F;&#x7406;&#x89E3;&#x4ED6;&#x7684;&#x60C5;&#x7EEA;&#xFF0C;&#x8BA4;&#x8BC6;&#x5230;&#x8FD9;&#x662F;&#x4ED6;&#x5BF9;&#x81EA;&#x6211;&#x80FD;&#x529B;&#x7684;&#x8D28;&#x7591;&#xFF0C;&#x4ED6;&#x5C31;&#x6709;&#x53EF;&#x80FD;&#x8F6C;&#x6362;&#x81EA;&#x5DF1;&#x7684;&#x601D;&#x7EF4;&#x65B9;&#x5F0F;&#x3002;&#x4ED6;&#x53EF;&#x4EE5;&#x5C1D;&#x8BD5;&#x6539;&#x53D8;&#x81EA;&#x5DF1;&#x7684;&#x5B66;&#x4E60;&#x7B56;&#x7565;&#xFF0C;&#x5BFB;&#x627E;&#x65B0;&#x7684;&#x65B9;&#x5F0F;&#x6765;&#x63D0;&#x9AD8;&#x4ED6;&#x7684;&#x6570;&#x5B66;&#x80FD;&#x529B;&#xFF0C;&#x800C;&#x4E0D;&#x662F;&#x4E00;&#x5473;&#x5730;&#x62B1;&#x6028;&#x3002;</li><li>&#x5728;&#x4EBA;&#x9645;&#x5173;&#x7CFB;&#x4E2D;&#xFF0C;&#x5047;&#x8BBE;&#x4E00;&#x4E2A;&#x670B;&#x53CB;&#x7ECF;&#x5E38;&#x62B1;&#x6028;&#x5979;&#x7684;&#x7537;&#x670B;&#x53CB;&#x4E0D;&#x591F;&#x5173;&#x5FC3;&#x5979;&#x3002;&#x5982;&#x679C;&#x5979;&#x80FD;&#x591F;&#x53CD;&#x601D;&#x81EA;&#x5DF1;&#x7684;&#x60C5;&#x7EEA;&#xFF0C;&#x5979;&#x53EF;&#x80FD;&#x4F1A;&#x53D1;&#x73B0;&#x5979;&#x7684;&#x62B1;&#x6028;&#x5176;&#x5B9E;&#x6E90;&#x4E8E;&#x5979;&#x5BF9;&#x88AB;&#x7231;&#x7684;&#x6E34;&#x671B;&#xFF0C;&#x800C;&#x5E76;&#x975E;&#x5979;&#x7684;&#x7537;&#x670B;&#x53CB;&#x771F;&#x7684;&#x4E0D;&#x5173;&#x5FC3;&#x5979;&#x3002;&#x7406;&#x89E3;&#x4E86;&#x8FD9;&#x4E00;&#x70B9;&#xFF0C;&#x5979;&#x53EF;&#x4EE5;&#x9009;&#x62E9;&#x548C;&#x7537;&#x670B;&#x53CB;&#x8FDB;&#x884C;&#x5F00;&#x653E;&#x548C;&#x8BDA;&#x5B9E;&#x7684;&#x6C9F;&#x901A;&#xFF0C;&#x8868;&#x8FBE;&#x81EA;&#x5DF1;&#x7684;&#x9700;&#x8981;&#xFF0C;&#x800C;&#x4E0D;&#x662F;&#x6301;&#x7EED;&#x62B1;&#x6028;&#x3002;</li></ol><p>&#xFF08;&#x7531; GPT4 &#x751F;&#x6210;&#xFF09;</p><p>&#x4E00;&#x4F4D;&#x670B;&#x53CB;&#x66FE;&#x7ECF;&#x751F;&#x52A8;&#x7684;&#x603B;&#x7ED3;&#x4E86;&#x8FD9;&#x79CD;&#x73B0;&#x8C61;&#xFF1A;&#x300C;&#x522B;&#x4EBA;&#x90FD;&#x6CA1;&#x600E;&#x4E48;&#x4F60;&#xFF0C;&#x4F60;&#x5148;&#x81EA;&#x7206;&#x4E86;&#x300D;&#x3002;&#x6211;&#x4EEC;&#x7684;&#x60C5;&#x7EEA;&#x5F80;&#x5F80;&#x6BD4;&#x5916;&#x754C;&#x4E8B;&#x5B9E;&#x4EA7;&#x751F;&#x7740;&#x66F4;&#x4E3A;&#x91CD;&#x5927;&#x7684;&#x5F71;&#x54CD;&#x3002;&#x800C;&#x8FD9;&#xFF0C;&#x5F88;&#x5927;&#x7A0B;&#x5EA6;&#x4E0A;&#x90FD;&#x6E90;&#x4E8E;&#x81EA;&#x5351;&#x611F;&#x548C;&#x4E0D;&#x5B89;&#x5168;&#x611F;&#x3002;&#x6211;&#x4EEC;&#x4E3A;&#x4E86;&#x5408;&#x7406;&#x5316;&#x81EA;&#x5DF1;&#x7684;&#x81EA;&#x5351;&#x548C;&#x4E0D;&#x5B89;&#x5168;&#x611F;&#xFF0C;&#x5F80;&#x5F80;&#x52A0;&#x5927;&#x529B;&#x5EA6;&#x8FDB;&#x884C;&#x7740;&#x8111;&#x8865;&#x548C;&#x63A8;&#x7406;&#xFF0C;&#x6700;&#x540E;&#x5F97;&#x51FA;&#x53EF;&#x4EE5;&#x4F50;&#x8BC1;&#x81EA;&#x5DF1;&#x4E0D;&#x5B89;&#x5168;&#x611F;&#x7684;&#x7ED3;&#x8BBA;&#x3002;&#x4F46;&#x957F;&#x6B64;&#x4EE5;&#x5F80;&#xFF0C;&#x6211;&#x4EEC;&#x7684;&#x4E0D;&#x5B89;&#x5168;&#x611F;&#x548C;&#x81EA;&#x5351;&#x611F;&#x4E5F;&#x8FDB;&#x4E00;&#x6B65;&#x52A0;&#x6DF1;&#x3002;&#x800C;&#x5F53;&#x574F;&#x7ED3;&#x679C;&#x53D1;&#x751F;&#x4E4B;&#x65F6;&#xFF0C;&#x81EA;&#x6211;&#x4F50;&#x8BC1;&#x7684;&#x9884;&#x8A00;&#xFF08;self-proving prophecy&#xFF09;&#x4E5F;&#x5F97;&#x4EE5;&#x5B9E;&#x73B0;&#x3002;</p><p></p>]]></content:encoded></item><item><title><![CDATA[Setting up Nuxt.js and Strapi applications and deploying them to the server]]></title><description><![CDATA[<p>Nuxt.js is a popular framework that enhances the capabilities of Vue.js for building websites. It provides developers with guidelines and restrictions to simplify the website creation process. It is particularly well-suited for creating content-rich websites such as company websites or blogs. By leveraging the features of Vue.js,</p>]]></description><link>https://sophiecao.me/setting-up-nuxtjs-and-strapi-application-and-deploying-to-server/</link><guid isPermaLink="false">645575e499b223000110efcb</guid><category><![CDATA[Backend]]></category><category><![CDATA[DevOps]]></category><category><![CDATA[Frontend]]></category><category><![CDATA[Nuxt.js]]></category><category><![CDATA[Strapi]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Sun, 07 May 2023 19:00:12 GMT</pubDate><content:encoded><![CDATA[<p>Nuxt.js is a popular framework that enhances the capabilities of Vue.js for building websites. It provides developers with guidelines and restrictions to simplify the website creation process. It is particularly well-suited for creating content-rich websites such as company websites or blogs. By leveraging the features of Vue.js, Nuxt.js enables developers to build dynamic and engaging web experiences with ease.</p><p>Strapi is an open-source headless CMS built on Node.js, designed for efficient content modelling and management. It offers a user-friendly interface, allowing developers to define and structure content types easily. With its headless architecture, Strapi separates the content management system from the presentation layer, enabling flexible API consumption. It is a customizable CMS focused on content types, making it ideal for content-driven applications and websites.</p><p>PM2 is an open-source process manager for Node.js applications that simplifies management and deployment. It offers process monitoring, automatic restarts, load balancing, and log management. With PM2, developers can easily manage their Node.js applications in production.</p><h2 id="setting-up-the-project">Setting up the project</h2><p>Firstly, we have to set up both Nuxt.js and Strapi, which will be referred to as frontend and backend.</p><pre><code class="language-bash"># setting up Nuxt.js

mkdir nuxt-strapi
npx nuxi init frontend
cd frontend
yarn install

# setting up Strapi
yarn create strapi-app backend</code></pre><p>After this, we will configure PM2 to handle the management of both Nuxt.js and Strapi. This setup will provide a centralized solution for managing the environment variables as well.</p><p><code>ecosystem.config.js</code>:</p><pre><code class="language-javascript">const env = process.argv[process.argv.indexOf(&apos;--env&apos;) + 1];
const isDev = env === &apos;dev&apos;;

module.exports = {
  apps: [
    // Frontend - Nuxt 3
    {
      name: &apos;frontend&apos;,
      cwd: &apos;./frontend&apos;, // Adjust the path to your frontend project directory
      append_env_to_name: true,
      script: &apos;yarn&apos;,
      args: isDev ? &apos;dev&apos; : &apos;start&apos;,
      interpreter: &apos;none&apos;,
      env_dev: {
        NODE_ENV: &apos;development&apos;,
        HOST: &apos;0.0.0.0&apos;,
        STRAPI_URL: &apos;http://127.0.0.1:1337&apos;,
        PORT: 3000, // Choose the port you want to use for your frontend
      },
      watch: false,
      max_memory_restart: &apos;1G&apos;,
      instances: 1, // Only one instance
      hooks: {
        pre_start: {
          command: &apos;yarn&apos;,
          args: &apos;build&apos;, // Build the frontend before starting
        },
      },
    },
    // Backend - Strapi
    {
      append_env_to_name: true,
      name: &apos;backend&apos;,
      cwd: &apos;./backend&apos;, 
      script: &apos;yarn&apos;,
      args: isDev ? &apos;develop&apos; : &apos;start&apos;,
      interpreter: &apos;none&apos;,
      env_dev: {
        NODE_ENV: &apos;development&apos;,
        HOST: &apos;0.0.0.0&apos;,
        PORT: &apos;1337&apos;,
        URL: &apos;http://127.0.0.1:1337&apos;,
        APP_KEYS: &quot;toBeModified1,toBeModified2&quot;,
        API_TOKEN_SALT: &apos;tobemodified&apos;,
        ADMIN_JWT_SECRET: &apos;tobemodified&apos;,
        JWT_SECRET: &apos;tobemodified&apos;,
        DATABASE_CLIENT: &apos;sqlite&apos;,
        DATABASE_FILENAME: &apos;./tmp/data.db&apos;
      },
      watch: false,
      max_memory_restart: &apos;1G&apos;,
      instances: 1, // Only one instance
      hooks: {
        pre_start: {
          command: &apos;yarn&apos;,
          args: &apos;build&apos;, // Build the backend before starting
        },
      },
    },
  ],
};</code></pre><p>Then, we can use the following command to start both the frontend and the backend.</p><pre><code class="language-bash">pm2 start ecosystem.config.js --env dev</code></pre><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://pm2.keymetrics.io/docs/usage/application-declaration/?ref=sophiecao.me"><div class="kg-bookmark-content"><div class="kg-bookmark-title">PM2 - Ecosystem File</div><div class="kg-bookmark-description">Advanced process manager for production Node.js applications. Load balancer, logs facility, startup script, micro service management, at a glance.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://pm2.keymetrics.io/favicon-96x96.png" alt><span class="kg-bookmark-author">PM2 logo</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://raw.githubusercontent.com/unitech/pm2/master/pres/pm2.20d3ef.png" alt></div></a></figure><p>To let the Nuxt.js interact with Strapi, we can add <code>@nuxtjs/strapi</code> dependency.</p><pre><code class="language-bash">yarn add --dev @nuxtjs/strapi</code></pre><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://strapi.nuxtjs.org/setup?ref=sophiecao.me"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Setup &#xB7; Nuxt Strapi</div><div class="kg-bookmark-description">Learn how to setup strapi module in your Nuxt 3 application.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://strapi.nuxtjs.org/favicon.ico" alt></div></div><div class="kg-bookmark-thumbnail"><img src="https://strapi.nuxtjs.org/cover.png" alt></div></a></figure><h2 id="loading-the-env-file-into-the-pm2-configuration">Loading the <code>.env</code> file into the PM2 configuration</h2><p>Although PM2 provides a nice unified entrypoint of managing the env variables and the applications, it is currently not possible to load environment variables directly from the <code>.env</code> file. Therefore I wrote an npm package to resolve this problem.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.npmjs.com/package/pm2-dotenv?ref=sophiecao.me"><div class="kg-bookmark-content"><div class="kg-bookmark-title">pm2-dotenv</div><div class="kg-bookmark-description">A package to load environment variables from .env files for PM2 ecosystem.config.js. Latest version: 0.3.0, last published: 2 days ago. Start using pm2-dotenv in your project by running `npm i pm2-dotenv`. There are no other projects in the npm registry using pm2-dotenv.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://static.npmjs.com/1996fcfdf7ca81ea795f67f093d7f449.png" alt><span class="kg-bookmark-author">npm</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://static.npmjs.com/338e4905a2684ca96e08c7780fc68412.png" alt></div></a></figure><p>Modify the <code>ecosystem.config.js</code>:</p><pre><code class="language-javascript">const { injectEnvs } = require(&apos;pm2-dotenv&apos;)

module.exports = {
    apps: [
        {
            name: &apos;frontend&apos;,
            
            // add this line
            ...injectEnvs(&apos;frontend&apos;)
        },
        {
            name: &apos;backend&apos;,
            ...injectEnvs(&apos;backend&apos;)
        }
    ]
}</code></pre><p></p><p>Now you can place all the variables in a <code>.env.production</code> file and let pm2 loads them:</p><pre><code class="language-env">FRONTEND_NODE_ENV=&apos;production&apos;
FRONTEND_HOST=&apos;0.0.0.0&apos;
FRONTEND_STRAPI_URL=&apos;http://127.0.0.1:1337&apos;
FRONTEND_PORT=3000

BACKEND_NODE_ENV=&apos;production&apos;
BACKEND_HOST=&apos;0.0.0.0&apos;
BACKEND_PORT=&apos;1337&apos;
BACKEND_URL=&apos;http://127.0.0.1:1337&apos;
BACKEND_APP_KEYS=&quot;toBeModified1toBeModified2&quot;
BACKEND_API_TOKEN_SALT=&apos;tobemodified&apos;
BACKEND_ADMIN_JWT_SECRET=&apos;tobemodified&apos;
BACKEND_JWT_SECRET=&apos;tobemodified&apos;
BACKEND_DATABASE_CLIENT=&apos;sqlite&apos;
BACKEND_DATABASE_FILENAME=&apos;./tmp/data.db&apos;</code></pre><p>Note that you may change the database connection from SQLite to MySQL. However, you might have to modify the <code>config/database.js</code> file in the Strapi project.</p><pre><code class="language-javascript">
const path = require(&apos;path&apos;);

module.exports = ({ env }) =&gt; {
  const client = env(&apos;DATABASE_CLIENT&apos;, &apos;sqlite&apos;);

  const connections = {
    mysql: {
      connection: {
        connectionString: env(&apos;DATABASE_URL&apos;),
        host: env(&apos;DATABASE_HOST&apos;, &apos;localhost&apos;),
        port: env.int(&apos;DATABASE_PORT&apos;, 3306),
        database: env(&apos;DATABASE_NAME&apos;, &apos;strapi&apos;),
        user: env(&apos;DATABASE_USERNAME&apos;, &apos;strapi&apos;),
        password: env(&apos;DATABASE_PASSWORD&apos;, &apos;strapi&apos;),
        ssl: env.bool(&apos;DATABASE_SSL&apos;, false) &amp;&amp; {
          key: env(&apos;DATABASE_SSL_KEY&apos;, undefined),
          cert: env(&apos;DATABASE_SSL_CERT&apos;, undefined),
          ca: env(&apos;DATABASE_SSL_CA&apos;, undefined),
          capath: env(&apos;DATABASE_SSL_CAPATH&apos;, undefined),
          cipher: env(&apos;DATABASE_SSL_CIPHER&apos;, undefined),
          rejectUnauthorized: env.bool(
            &apos;DATABASE_SSL_REJECT_UNAUTHORIZED&apos;,
            true
          ),
        },
      },
      pool: { min: env.int(&apos;DATABASE_POOL_MIN&apos;, 2), max: env.int(&apos;DATABASE_POOL_MAX&apos;, 10) },
    },
    sqlite: {
      connection: {
        filename: path.join(
          __dirname,
          &apos;..&apos;,
          env(&apos;DATABASE_FILENAME&apos;, &apos;./tmp/data.db&apos;)
        ),
      },
      useNullAsDefault: true,
    },
  };

  return {
    connection: {
      client,
      ...connections[client],
      acquireConnectionTimeout: env.int(&apos;DATABASE_CONNECTION_TIMEOUT&apos;, 60000),
    },
  };
};</code></pre><p>Also, you might have to change the <code>config/server.js</code>:</p><pre><code class="language-javascript">module.exports = ({ env }) =&gt; ({
  host: env(&apos;HOST&apos;, &apos;0.0.0.0&apos;),
  port: env.int(&apos;PORT&apos;, 1337),
  url: env(&apos;URL&apos;, &apos;http://0.0.0.0:1337&apos;),
  app: {
    keys: env.array(&apos;APP_KEYS&apos;),
  },
  webhooks: {
    populateRelations: env.bool(&apos;WEBHOOKS_POPULATE_RELATIONS&apos;, false),
  },
});</code></pre><p>Finally, start the processes:</p><pre><code class="language-bash">pm2 start ecosystem.config.js --env production</code></pre><h2 id="use-nginx-for-reverse-proxy">Use Nginx for reverse proxy</h2><p>When all is set, the final step is to use nginx to proxy the request. You may add the following configuration to your existing nginx config. Note that in this example we put the Strapi to the <code>/api</code> path.</p><pre><code>upstream strapi {
    server localhost:1337;
}

upstream nuxt {
    server localhost:3000;
}

server {
    listen 80;

    gzip            on;
    gzip_types      text/plain application/xml text/css application/javascript;
    gzip_min_length 1000;

    # Static Root
    location / {
        expires $expires;
        proxy_redirect                      off;
        proxy_set_header Host               $host;
        proxy_set_header X-Real-IP          $remote_addr;
        proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto  $scheme;
        proxy_read_timeout          1m;
        proxy_connect_timeout       1m;
        proxy_pass                          http://nuxt;
    }

    # Strapi API and Admin
    location /api/ {
        rewrite ^/api/?(.*)$ /$1 break;
        proxy_pass http://strapi;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection &quot;Upgrade&quot;;
        proxy_pass_request_headers on;
    }
}</code></pre>]]></content:encoded></item><item><title><![CDATA[How to set-up Traefik as a reverse proxy for Docker containers]]></title><description><![CDATA[<p>When using docker for managing services, one important thing is how to publish the service to the internet, this typically requires configuring a domain, obtaining an &#xA0;SSL certificate, and other related tasks. However, performing these tasks manually can be tedious and time-consuming.</p><h2 id="the-common-approach-for-publishing-service">The common approach for publishing service</h2><p>Nginx</p>]]></description><link>https://sophiecao.me/configure-traefik-to-reverse-proxying-docker-containers/</link><guid isPermaLink="false">64554f8b99b223000110ef05</guid><category><![CDATA[DevOps]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Traefik]]></category><dc:creator><![CDATA[Sophie Cao]]></dc:creator><pubDate>Fri, 05 May 2023 19:24:51 GMT</pubDate><content:encoded><![CDATA[<p>When using docker for managing services, one important thing is how to publish the service to the internet, this typically requires configuring a domain, obtaining an &#xA0;SSL certificate, and other related tasks. However, performing these tasks manually can be tedious and time-consuming.</p><h2 id="the-common-approach-for-publishing-service">The common approach for publishing service</h2><p>Nginx is a widely-used open-source web server frequently used as a reverse proxy server. The standard configuration for using Nginx as a reverse proxy outside of Docker typically involves defining an upstream server block that specifies the IP address and port number of the backend server and then using the <code>proxy_pass</code> directive to forward incoming requests to the backend server.</p><pre><code>server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}</code></pre><p>When using Docker, if Nginx and the container are connected to the same Docker network, Nginx can communicate with the container using the container&apos;s hostname. Therefore, the configuration could be rewritten to this:</p><pre><code>server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend-container:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}</code></pre><h2 id="service-publishing-with-traefik">Service publishing with Traefik</h2><p>Traefik is a modern reverse proxy that is designed for making deploying services simple. It can use service discovery to configure the services and can work with Let&apos;s Encrypt to automatically generate certificates.</p><p><code>docker-compose.yml</code>:</p><pre><code class="language-yaml">version: &apos;3.9&apos;

services:
    traefik:
        image: &quot;traefik:v2.6&quot;
        container_name: &quot;traefik&quot;
        ports:
            - &quot;80:80&quot;
            - &quot;443:443&quot;
            - &quot;8080:8080&quot;
        volumes:
            - &quot;./traefik.toml:/etc/traefik/traefik.toml&quot;
            - &quot;./letsencrypt:/letsencrypt&quot;
            - &quot;/var/run/docker.sock:/var/run/docker.sock:ro&quot;
        networks:
            outbound:
                name: outbound</code></pre><p><code>traefik.toml</code>:</p><pre><code class="language-toml">[entryPoints]
  [entryPoints.web]
    address = &quot;:80&quot;

  [entryPoints.web.forwardedHeaders]
    insecure = true

  [entryPoints.web.http.redirections.entryPoint]
    to = &quot;websecure&quot;
    scheme = &quot;https&quot;

  [entryPoints.websecure]
    address = &quot;:443&quot;

[certificatesResolvers.letsencrypt.acme]
  email = &quot;janedoe@example.com&quot;
  storage = &quot;/letsencrypt/acme.json&quot;
  [certificatesResolvers.letsencrypt.acme.tlsChallenge]

[api]
  dashboard = true
  insecure = true

[providers.docker]
  exposedByDefault = false
  watch = true
  network = &quot;outbound&quot;</code></pre><p>Finally, you need to create an empty <code>letsencrypt/acme.json</code> file. Then when we want to publish the service, we can do the following:</p><pre><code class="language-yaml">version: &apos;3.9&apos;

services:
    backend:
        image: backend
        labels:
            - traefik.enable=true
            - traefik.docker.network=outbound
            - traefik.http.routers.backend.rule=Host(`example.com`)
            - traefik.http.routers.backend.entrypoints=websecure
            - traefik.http.routers.backend.tls.certresolver=letsencrypt
        networks:
            - outbound


networks:
    outbound:
        external: true
        name: outbound</code></pre>]]></content:encoded></item></channel></rss>