<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>rundiz</title>
	<atom:link href="https://rundiz.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://rundiz.com/</link>
	<description>Share the source code. Offers premium products.</description>
	<lastBuildDate>Sun, 21 Dec 2025 19:39:47 +0000</lastBuildDate>
	<language>th</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://rundiz.com/wp-content/uploads/2022/04/cropped-rundiz-logo-512x512-1-32x32.png</url>
	<title>rundiz</title>
	<link>https://rundiz.com/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Stripe subscription และสถานะใบเรียกเก็บเงิน</title>
		<link>https://rundiz.com/blog/articles/stripe-subscription-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%aa%e0%b8%96%e0%b8%b2%e0%b8%99%e0%b8%b0%e0%b9%83%e0%b8%9a%e0%b9%80%e0%b8%a3%e0%b8%b5%e0%b8%a2%e0%b8%81%e0%b9%80%e0%b8%81%e0%b9%87%e0%b8%9a%e0%b9%80/</link>
					<comments>https://rundiz.com/blog/articles/stripe-subscription-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%aa%e0%b8%96%e0%b8%b2%e0%b8%99%e0%b8%b0%e0%b9%83%e0%b8%9a%e0%b9%80%e0%b8%a3%e0%b8%b5%e0%b8%a2%e0%b8%81%e0%b9%80%e0%b8%81%e0%b9%87%e0%b8%9a%e0%b9%80/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Thu, 30 Oct 2025 05:30:02 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[ecommerce]]></category>
		<category><![CDATA[stripe]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=1058</guid>

					<description><![CDATA[<p>การตั้งค่าของการเรียกชำระเงินแบบ subscription บน Stripe &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/stripe-subscription-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%aa%e0%b8%96%e0%b8%b2%e0%b8%99%e0%b8%b0%e0%b9%83%e0%b8%9a%e0%b9%80%e0%b8%a3%e0%b8%b5%e0%b8%a2%e0%b8%81%e0%b9%80%e0%b8%81%e0%b9%87%e0%b8%9a%e0%b9%80/">Stripe subscription และสถานะใบเรียกเก็บเงิน</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>การตั้งค่าของการเรียกชำระเงินแบบ subscription บน Stripe.com นั้นค่อนข้างจะมีปัญหากันมาก และที่พบบ่อยคือ แม้เมื่อรายการ subscription นั้นๆจะถูกยกเลิกไปแล้ว (canceled) จากการเรียกเก็บเงินไม่สำเร็จ แต่ในหน้า customer portal ยังคงมีการเรียกเก็บเงินอยู่ดี และเมื่อลูกค้าชำระเงินก็จะพบว่าเป็นการชำระไปยังรายการเรียกเก็บเงินเก่า แต่สถานะ subscription อาจยังคงเป็นการยกเลิกเหมือนเดิมไม่เปลี่ยนแปลงแม้จะไล่ชำระจนหมดแล้วก็ตาม.</p>
<p>ด้วยเหตุนี้เราจึงจะมาทดสอบการตั้งค่าต่างๆ เพื่อดูว่ามันจะมีผลเป็นอย่างไรในหน้า customer portal.</p>
<h2>การทดสอบ</h2>
<p>โค้ดที่ใช้ในการทดสอบ checkout.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-php">$checkoutParams = [
    'line_items' =&gt; [[
        'price_data' =&gt; [
            'currency' =&gt; 'thb',
            'product_data' =&gt; [
                'name' =&gt; 'Maid',
            ],
            'recurring' =&gt; [
                'interval' =&gt; 'day',
                'interval_count' =&gt; 1,
            ],
            'unit_amount' =&gt; 30000,
        ],
        'quantity' =&gt; 1,
    ]],
    'mode' =&gt; 'subscription',
    'customer_email' =&gt; 'user@test.localhost',
    'success_url' =&gt; 'success.html',
    'cancel_url' =&gt; 'canceled.html',
];
$checkout_session = \Stripe\Checkout\Session::create($checkoutParams);</code></pre>
<p>การตั้งค่า Manage failed payments ให้ไปยังหน้า Settings แล้วคลิกเลือกที่เมนู Billing &gt; <a href="https://dashboard.stripe.com/settings/billing/subscriptions" target="_blank" rel="noopener">Subscriptions and emails</a>.</p>
<p>ในขั้นตอนการทดสอบ ให้เริ่มชำระเงินด้วย<a href="https://docs.stripe.com/testing" target="_blank" rel="noopener">บัตรที่ใช้ได้ดังในหน้าเอกสาร</a>. จากนั้นก่อนที่จะ Simulation ไปยังวันถัดไป ให้กดเข้า <a href="https://docs.stripe.com/no-code/customer-portal" target="_blank" rel="noopener">customer portal</a> แล้วแก้ไขโดยเพิ่มบัตรที่ใช้ไม่ได้ ( Decline after attaching ) แล้วลบบัตรที่ใช้ได้ออก จากนั้นจึงค่อย Simulation ไปยังวันถัดไป.</p>
<h3>Subscription: cancel; Invoice: past-due</h3>
<figure id="attachment_1060" aria-describedby="caption-attachment-1060" style="width: 677px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue.webp"><img fetchpriority="high" decoding="async" class="img-fluid wp-image-1060 size-full" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue.webp" alt="" width="677" height="379" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue.webp 677w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue-300x168.webp 300w" sizes="(max-width: 677px) 100vw, 677px" /></a><figcaption id="caption-attachment-1060" class="wp-caption-text">Subscription: cancel; Invoice: past-due</figcaption></figure>
<figure id="attachment_1062" aria-describedby="caption-attachment-1062" style="width: 598px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_dashboard.webp"><img decoding="async" class="img-fluid wp-image-1062 size-large" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_dashboard-598x1024.webp" alt="" width="598" height="1024" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_dashboard-598x1024.webp 598w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_dashboard-175x300.webp 175w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_dashboard-768x1315.webp 768w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_dashboard-897x1536.webp 897w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_dashboard.webp 972w" sizes="(max-width: 598px) 100vw, 598px" /></a><figcaption id="caption-attachment-1062" class="wp-caption-text">Dashboard</figcaption></figure>
<figure id="attachment_1061" aria-describedby="caption-attachment-1061" style="width: 581px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_customer-portal.webp"><img decoding="async" class="img-fluid wp-image-1061 size-full" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_customer-portal.webp" alt="" width="581" height="845" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_customer-portal.webp 581w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-pastdue_customer-portal-206x300.webp 206w" sizes="(max-width: 581px) 100vw, 581px" /></a><figcaption id="caption-attachment-1061" class="wp-caption-text">Customer portal</figcaption></figure>
<p>จากตัวอย่างจะพบว่าแม้สถานะ subscription จะยกเลิกไปแล้วแต่หน้า customer portal ก็จะยังคงมีปุ่มเรียกเก็บเงิน และจากที่เคยทดสอบมา เมื่อกดชำระจนครบก็จะไม่เปลี่ยนสถานะ subscription ให้กลับมาเป็น active ได้อยู่ดี.</p>
<h3>Subscription: cancel; Invoice: uncollectible</h3>
<figure id="attachment_1063" aria-describedby="caption-attachment-1063" style="width: 677px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1063 size-full" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible.webp" alt="" width="677" height="467" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible.webp 677w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible-300x207.webp 300w" sizes="auto, (max-width: 677px) 100vw, 677px" /></a><figcaption id="caption-attachment-1063" class="wp-caption-text">Subscription: cancel; Invoice: uncollectible</figcaption></figure>
<figure id="attachment_1065" aria-describedby="caption-attachment-1065" style="width: 606px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_dashboard.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1065 size-large" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_dashboard-606x1024.webp" alt="" width="606" height="1024" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_dashboard-606x1024.webp 606w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_dashboard-177x300.webp 177w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_dashboard-768x1298.webp 768w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_dashboard-909x1536.webp 909w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_dashboard.webp 972w" sizes="auto, (max-width: 606px) 100vw, 606px" /></a><figcaption id="caption-attachment-1065" class="wp-caption-text">Dashboard</figcaption></figure>
<p>สิ่งที่แตกต่างออกไปคือ สถานะใบเรียกเก็บเงินจะเป็น uncollectible แค่นั้น.</p>
<figure id="attachment_1064" aria-describedby="caption-attachment-1064" style="width: 581px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_customer-portal.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1064 size-full" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_customer-portal.webp" alt="" width="581" height="833" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_customer-portal.webp 581w, https://rundiz.com/wp-content/uploads/2025/10/subscription-cancel-invoice-uncollectible_customer-portal-209x300.webp 209w" sizes="auto, (max-width: 581px) 100vw, 581px" /></a><figcaption id="caption-attachment-1064" class="wp-caption-text">Customer portal</figcaption></figure>
<p>จากการทดสอบนี้ พบว่า หน้า customer portal จะยังคงมีปุ่มเรียกเก็บเงินเช่นเดียวกัน เนื่องจากตั้งค่าสถานะ payment fail สำหรับ subscription เป็น cancel. และการชำระเงินจนครบก็ไม่ช่วยเปลี่ยนสถานะเช่นเคย.</p>
<h3>Subscription: unpaid; Invoice: past-due</h3>
<figure id="attachment_1066" aria-describedby="caption-attachment-1066" style="width: 677px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1066 size-full" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue.webp" alt="" width="677" height="335" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue.webp 677w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue-300x148.webp 300w" sizes="auto, (max-width: 677px) 100vw, 677px" /></a><figcaption id="caption-attachment-1066" class="wp-caption-text">Subscription: unpaid; Invoice: past-due</figcaption></figure>
<figure id="attachment_1068" aria-describedby="caption-attachment-1068" style="width: 435px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_dashboard.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1068 size-large" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_dashboard-435x1024.webp" alt="" width="435" height="1024" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_dashboard-435x1024.webp 435w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_dashboard-128x300.webp 128w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_dashboard-768x1807.webp 768w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_dashboard-653x1536.webp 653w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_dashboard-870x2048.webp 870w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_dashboard.webp 972w" sizes="auto, (max-width: 435px) 100vw, 435px" /></a><figcaption id="caption-attachment-1068" class="wp-caption-text">Dashboard</figcaption></figure>
<p>ในหน้า Dashboard นั้น จากการ Run simulation ข้ามเวลาไปเรื่อยๆจะพบว่าแม้เลยขีดกำหนดไปเรื่อยๆเท่าไหร่ มันก็จะยังคงสร้าง draft ของใบเรียกเก็บเงินออกมาอยู่เรื่อยไป และสถานะของ subscription จะเป็น Unpaid ไม่ใช่ Canceled.</p>
<figure id="attachment_1067" aria-describedby="caption-attachment-1067" style="width: 581px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_customer-portal.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1067 size-full" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_customer-portal.webp" alt="" width="581" height="832" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_customer-portal.webp 581w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-pastdue_customer-portal-209x300.webp 209w" sizes="auto, (max-width: 581px) 100vw, 581px" /></a><figcaption id="caption-attachment-1067" class="wp-caption-text">Customer portal</figcaption></figure>
<p>จากตัวอย่างการตั้งค่าแบบนี้ จะพบว่าในหน้า Customer portal รายการ subscription จะยังคงไม่ถูกยกเลิก มีเพียงรายการเรียกเก็บเงินที่แจ้งว่าล้มเหลว (Failed) และเมื่อทดลอง Run simulation ข้ามเวลาไปเรื่อยๆก็จะพบว่ามันยังคงอยู่เหมือนเดิมไม่มีการยกเลิก แต่จะมีปุ่มให้กดยกเลิกด้วยตัวเอง.</p>
<h3>Subscription: unpaid; Invoice: uncollectible</h3>
<figure id="attachment_1069" aria-describedby="caption-attachment-1069" style="width: 677px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1069 size-full" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible.webp" alt="" width="677" height="370" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible.webp 677w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible-300x164.webp 300w" sizes="auto, (max-width: 677px) 100vw, 677px" /></a><figcaption id="caption-attachment-1069" class="wp-caption-text">Subscription: unpaid; Invoice: uncollectible</figcaption></figure>
<figure id="attachment_1071" aria-describedby="caption-attachment-1071" style="width: 444px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_dashboard.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1071 size-large" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_dashboard-444x1024.webp" alt="" width="444" height="1024" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_dashboard-444x1024.webp 444w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_dashboard-130x300.webp 130w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_dashboard-768x1771.webp 768w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_dashboard-666x1536.webp 666w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_dashboard-888x2048.webp 888w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_dashboard.webp 972w" sizes="auto, (max-width: 444px) 100vw, 444px" /></a><figcaption id="caption-attachment-1071" class="wp-caption-text">Dashboard</figcaption></figure>
<p>ในหน้า Dashboard จะพบว่าแม้เลยกำหนดชำระเงินไปนานเท่าใด ก็จะยังคงสร้าง draft ใบเรียกเก็บเงินออกมาเรื่อยๆ.</p>
<figure id="attachment_1070" aria-describedby="caption-attachment-1070" style="width: 581px" class="wp-caption aligncenter"><a href="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_customer-portal.webp"><img loading="lazy" decoding="async" class="img-fluid wp-image-1070 size-full" src="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_customer-portal.webp" alt="" width="581" height="832" srcset="https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_customer-portal.webp 581w, https://rundiz.com/wp-content/uploads/2025/10/subscription-unpaid-invoice-uncollectible_customer-portal-209x300.webp 209w" sizes="auto, (max-width: 581px) 100vw, 581px" /></a><figcaption id="caption-attachment-1070" class="wp-caption-text">Customer portal</figcaption></figure>
<p>จากตัวอย่างจะพบว่าสิ่งที่แตกต่างจากการทดลองก่อนหน้าคือสถานะใบเรียกเก็บเงินจะเป็น Past due แทนที่จะเป็น Failed แต่สถานะ subscription จะยังคงไม่ยกเลิกและมีปุ่มให้ยกเลิกด้วยตัวเองเช่นเดิม.</p>
<h3>การแก้ไข</h3>
<p>สำหรับการแก้ไขให้ยกเลิกใบเรียกเก็บเงินนั้น ณ ปัจจุบัน จะยังคงไม่สามารถทำได้ผ่านการตั้งค่าบน Stripe แต่จะต้องใช้การโค้ดผ่าน webhooks ดังกรณีศึกษาจาก 2 เว็บไซต์ต่อไปนี้.</p>
<ol>
<li><a href="https://stackoverflow.com/questions/77981419/how-to-void-a-invoice-automatically-in-stripe-after-automated-retries-have-faile" target="_blank" rel="noopener">https://stackoverflow.com/questions/77981419/how-to-void-a-invoice-automatically-in-stripe-after-automated-retries-have-faile</a></li>
<li><a href="https://www.reddit.com/r/stripe/comments/1f9xtnq/the_option_mark_the_invoice_as_uncollectible/" target="_blank" rel="noopener">https://www.reddit.com/r/stripe/comments/1f9xtnq/the_option_mark_the_invoice_as_uncollectible/</a></li>
</ol>
<p>The post <a href="https://rundiz.com/blog/articles/stripe-subscription-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%aa%e0%b8%96%e0%b8%b2%e0%b8%99%e0%b8%b0%e0%b9%83%e0%b8%9a%e0%b9%80%e0%b8%a3%e0%b8%b5%e0%b8%a2%e0%b8%81%e0%b9%80%e0%b8%81%e0%b9%87%e0%b8%9a%e0%b9%80/">Stripe subscription และสถานะใบเรียกเก็บเงิน</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/stripe-subscription-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%aa%e0%b8%96%e0%b8%b2%e0%b8%99%e0%b8%b0%e0%b9%83%e0%b8%9a%e0%b9%80%e0%b8%a3%e0%b8%b5%e0%b8%a2%e0%b8%81%e0%b9%80%e0%b8%81%e0%b9%87%e0%b8%9a%e0%b9%80/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Pagination JS</title>
		<link>https://rundiz.com/blog/downloads/pagination-js/</link>
					<comments>https://rundiz.com/blog/downloads/pagination-js/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Wed, 03 Sep 2025 08:06:12 +0000</pubDate>
				<category><![CDATA[ดาวน์โหลด]]></category>
		<category><![CDATA[javascript]]></category>
		<category><![CDATA[node.js]]></category>
		<category><![CDATA[pagination]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=1048</guid>

					<description><![CDATA[<p>rd-pagination.js คือไลบรารี่แบ่งหน้าเขียนด้วยภาษา JavaS &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/downloads/pagination-js/">Pagination JS</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>rd-pagination.js คือไลบรารี่แบ่งหน้าเขียนด้วยภาษา JavaScript. คุณสามารถดาวน์โหลดติดตั้งผ่าน npm ได้ที่<a href="https://www.npmjs.com/package/rd-pagination.js" target="_blank" rel="noopener">แพ็คเกจ rd-pagination.js</a>.</p>
<h2>การใช้งาน</h2>
<p>กำหนดแท็ก HTML</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-markup">&lt;nav class="rd-pagination" aria-label="Page navigation example"&gt;&lt;/nav&gt;
&lt;script src="assets/js/rd-pagination.js"&gt;&lt;/script&gt;</code></pre>
<p>จากนั้นแทรก JavaScript โค้ดต่อไปนี้ในหน้าที่จะใช้งาน</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-javascript">const rdPagination = new RdPagination('.rd-pagination', {
    base_url: '?start=%PAGENUMBER%',
    page_number_value: 0,
    total_records: 195,
});
rdPagination.createLinks();</code></pre>
<h2>ส่วนประกอบต่างๆ</h2>
<p><a href="https://rundiz.com/wp-content/uploads/2025/09/pagination-cover.webp"><img loading="lazy" decoding="async" class="img-responsive border aligncenter wp-image-1049 size-large" src="https://rundiz.com/wp-content/uploads/2025/09/pagination-cover-1024x768.webp" alt="" width="1024" height="768" srcset="https://rundiz.com/wp-content/uploads/2025/09/pagination-cover-1024x768.webp 1024w, https://rundiz.com/wp-content/uploads/2025/09/pagination-cover-300x225.webp 300w, https://rundiz.com/wp-content/uploads/2025/09/pagination-cover-768x576.webp 768w, https://rundiz.com/wp-content/uploads/2025/09/pagination-cover-1536x1152.webp 1536w, https://rundiz.com/wp-content/uploads/2025/09/pagination-cover-500x375.webp 500w, https://rundiz.com/wp-content/uploads/2025/09/pagination-cover-1000x750.webp 1000w, https://rundiz.com/wp-content/uploads/2025/09/pagination-cover.webp 2000w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a></p>
<ul>
<li>before unavailable คือเลขหน้าที่จะแสดงก่อนแสดงหน้าที่ขาดช่วงไป (unavailable). คุณสามารถกำหนดค่าจำนวนของมันได้ที่ property unavailable_before เช่น <code class="language-js">{unavailable_before: 1}</code></li>
<li>unavailable คือตำแหน่งที่เลขหน้าขาดช่วงไประหว่างเลขหน้าที่แสดงก่อน และเลขหน้าที่อยู่รอบๆหน้าปัจจุบัน. คุณสามารถกำหนดค่าตัวอักษรที่จะแสดงตรงนี้ได้ที่ property unavailable_text เช่น <code class="language-js">{unavailable_text: '..'}</code>;</li>
<li>adjacent pages คือเลขหน้าที่อยู่รอบๆหน้าปัจจุบัน. คุณสามารถกำหนดค่าจำนวนของมันได้ที่ property number_adjacent_pages เช่น <code class="language-js">{number_adjacent_pages: 3}</code></li>
<li>after unavailable ก็เหมือน before unavailable แต่เป็นเลขหน้าที่แสดงหลังจากหน้าที่ขาดช่วงไป (unavailable). คุณสามารถกำหนดค่าจำนวนของมันได้ที่ property unavailable_after เช่น <code class="language-js">{unavailable_after: 2}</code></li>
<li>สามารถอ่านเพิ่มเติมได้จาก options ใน constructor ของ JS class.</li>
</ul>
<h3>Options</h3>
<div class="table-responsive">
<table class="table table-bordered table-no-stripe">
<thead>
<tr>
<th>Param</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>options.base_url</td>
<td><code>string</code></td>
<td>The URL for use when generate page numbers with links (Required).<br />
Set the position where page numbers will be appears as URI segment or query string with <code>%PAGENUMBER%</code> placeholder.<br />
Example 1: <code>http://domain.tld/my-category/page/%PAGENUMBER%</code> This URL use page number as URI segment.<br />
Example 2: <code>http://domain.tld/my-category?page=%PAGENUMBER%</code> This URL use page number as query string.<br />
Example 3: <code>http://domain.tld/my-category?filter=some_filter_values&amp;amp;search=foobar&amp;amp;page=%PAGENUMBER%</code> This URL use page number as query string with other query strings in it, seperate each query string with <code>&amp;amp;</code> not just <code>&amp;</code>.<br />
Example 4: <code>http://domain.tld/my-category?start=%PAGENUMBER%</code> This URL use page number as query string but use start as the name.<br />
You have to get the page number value and set its value to this class by call the "page_number_value" option.</td>
</tr>
<tr>
<td>options.page_number_value</td>
<td><code>number</code></td>
<td>The current page number value (Required). This class cannot detect current page number automatically because of dynamic styles of URL. So, you have to manually set its value to this option.</td>
</tr>
<tr>
<td>options.total_records</td>
<td><code>number</code></td>
<td>The total number of records (Required). This means "all" records by conditions with out the "LIMIT" or slices commands.</td>
</tr>
<tr>
<td>options.items_per_page</td>
<td><code>number</code></td>
<td>The number of items that will be displaying per page. Such as number of articles to display in each page. Default is 10.</td>
</tr>
<tr>
<td>options.page_number_type</td>
<td><code>string</code></td>
<td>The page number type. The value can be <code>start_num</code> or <code>page_num</code>. Default is <code>start_num</code>. Start number or <code>start_num</code> also know as offset number. (eg. page number value will be 0, 10, 20, 30, ...) Page number or <code>page_num</code>. (eg. page number value will be 1, 2, 3, 4, ...)</td>
</tr>
<tr>
<td>options.current_page_link</td>
<td><code>boolean</code></td>
<td>Display current link at current page. Set to true to display, false not to display. Default is <code>false</code>.</td>
</tr>
<tr>
<td>options.current_page_link_attributes</td>
<td><code>object</code></td>
<td>The current page link attributes in object where key is attribute name. Example <code>{'class' =&gt; 'my class'}</code>. Must not contains <code>href</code>, <code>data-rd-pagination</code> attributes.</td>
</tr>
<tr>
<td>options.current_tag_close</td>
<td><code>string</code></td>
<td>The current page tag close. If you set to display current page, this will be placed after link to the current page. Default is empty.</td>
</tr>
<tr>
<td>options.current_tag_open</td>
<td><code>string</code></td>
<td>The current page tag open. If you set to display current page, this will be placed before link to the current page. Default is empty.</td>
</tr>
<tr>
<td>options.first_page_always_show</td>
<td><code>boolean</code></td>
<td>If you are at first page the first page link will not show if you set this value to false, if you set to true it will be always show the first page link. Default is <code>false</code>.</td>
</tr>
<tr>
<td>options.first_page_link_attributes</td>
<td><code>object</code></td>
<td>The first page link attributes in object where key is attribute name. Example <code>{'class' =&gt; 'my class'}</code>. Must not contains <code>href</code>, <code>data-rd-pagination</code> attributes.</td>
</tr>
<tr>
<td>options.first_page_text</td>
<td><code>string</code> | <code>false</code></td>
<td>The link text of the paginate that will go to the first page. Set to false to not displaying first page link.</td>
</tr>
<tr>
<td>options.first_tag_close</td>
<td><code>string</code></td>
<td>The first page tag close. If you set to display first page, this will be placed after link to the first page. Default is 1 space.</td>
</tr>
<tr>
<td>options.first_tag_open</td>
<td><code>string</code></td>
<td>The first page tag open. If you set to display first page, this will be placed before link to the first page. Default is 1 space.</td>
</tr>
<tr>
<td>options.last_page_always_show</td>
<td><code>boolean</code></td>
<td>If you are at last page the last page link will not show if you set this value to false, if you set to true it will be always show the last page link. Default is <code>false</code>.</td>
</tr>
<tr>
<td>options.last_page_link_attributes</td>
<td><code>object</code></td>
<td>The last page link attributes in object where key is attribute name. Example <code>{'class' =&gt; 'my class'}</code>. Must not contains <code>href</code>, <code>data-rd-pagination</code> attributes.</td>
</tr>
<tr>
<td>options.last_page_text</td>
<td><code>string</code> | <code>false</code></td>
<td>The link text of the paginate that will go to the last page. Set to false to not displaying last page link.</td>
</tr>
<tr>
<td>options.last_tag_close</td>
<td><code>string</code></td>
<td>The last page tag close. If you set to display last page, this will be placed after link to the last page. Defauls is 1 space.</td>
</tr>
<tr>
<td>options.last_tag_open</td>
<td><code>string</code></td>
<td>The last page tag open. If you set to display last page, this will be placed before link to the last page. Default is 1 space.</td>
</tr>
<tr>
<td>options.next_page_always_show</td>
<td><code>boolean</code></td>
<td>If you are at last page the next page link will not show if you set this value to false, if you set to true it will be always show the next page link. Default is <code>false</code>.</td>
</tr>
<tr>
<td>options.next_page_link_attributes</td>
<td><code>object</code></td>
<td>The next page link attributes in object where key is attribute name. Example <code>{'class' =&gt; 'my class'}</code>. Must not contains <code>href</code>, <code>data-rd-pagination</code> attributes.</td>
</tr>
<tr>
<td>options.next_page_text</td>
<td><code>string</code> | <code>false</code></td>
<td>The link text of the paginate that will go to the next page. Set to false to not displaying next page link.</td>
</tr>
<tr>
<td>options.next_tag_close</td>
<td><code>string</code></td>
<td>The next page tag close. If you set to display next page, this will be placed after link to the next page. Default is 1 space.</td>
</tr>
<tr>
<td>options.next_tag_open</td>
<td><code>string</code></td>
<td>The next page tag open. If you set to display next page, this will be placed before link to the next page. Default is 1 space.</td>
</tr>
<tr>
<td>options.number_adjacent_pages</td>
<td><code>number</code></td>
<td>The number of adjacent pages before and after the current page. Defaut is 5.</td>
</tr>
<tr>
<td>options.number_display</td>
<td><code>boolean</code></td>
<td>Display the page numbers or not. Set to true to display, false not to display. Default is <code>true</code>.</td>
</tr>
<tr>
<td>options.number_page_link_attributes</td>
<td><code>object</code></td>
<td>The number page link attributes in object where key is attribute name. Example <code>{'class' =&gt; 'my class'}</code>. Must not contains <code>href</code>, <code>data-rd-pagination</code> attributes.</td>
</tr>
<tr>
<td>options.number_tag_close</td>
<td><code>string</code></td>
<td>The page number tag close. If you set to display page number, this will be placed after link to the page number. Default is 1 space.</td>
</tr>
<tr>
<td>options.number_tag_open</td>
<td><code>string</code></td>
<td>The page number tag open. If you set to display page number, this will be placed before link to the page number. Default is 1 space.</td>
</tr>
<tr>
<td>options.overall_tag_close</td>
<td><code>string</code></td>
<td>The overall tag close. It will be place at the very end of displaying page numbers. Default is empty.</td>
</tr>
<tr>
<td>options.overall_tag_open</td>
<td><code>string</code></td>
<td>The overall tag open. It will be place at the very beginning of displaying page numbers. Default is empty.</td>
</tr>
<tr>
<td>options.previous_page_always_show</td>
<td><code>boolean</code></td>
<td>If you are at first page the previous page link will not show if you set this value to false, if you set to true it will be always show the previous page link. Default is <code>false</code>.</td>
</tr>
<tr>
<td>options.previous_page_link_attributes</td>
<td><code>object</code></td>
<td>The previous page link attributes in object where key is attribute name. Example <code>{'class' =&gt; 'my class'}</code>. Must not contains <code>href</code>, <code>data-rd-pagination</code> attributes.</td>
</tr>
<tr>
<td>options.previous_page_text</td>
<td><code>string</code> | <code>false</code></td>
<td>The link text of the paginate that will go to the previous page. Set to false to not displaying previous page link.</td>
</tr>
<tr>
<td>options.previous_tag_close</td>
<td><code>string</code></td>
<td>The previous page tag close. If you set to display previous page, this will be placed after link to the previous page. Default is 1 space.</td>
</tr>
<tr>
<td>options.previous_tag_open</td>
<td><code>string</code></td>
<td>The previous page tag open. If you set to display previous page, this will be placed before link to the previous page. Default is 1 space.</td>
</tr>
<tr>
<td>options.unavailable_display</td>
<td><code>boolean</code></td>
<td>Display unavailable page (...) or not. Set to true to display, false to not display. Default is <code>false</code>.</td>
</tr>
<tr>
<td>options.unavailable_text</td>
<td><code>string</code></td>
<td>The unavailable page text. Basically it is something to show that there are pages between these range such as 3 dots text. (...) Default is <code>&amp;hellip;</code>.</td>
</tr>
<tr>
<td>options.unavailable_tag_close</td>
<td><code>string</code></td>
<td>The unavailable page tag close. If you set to display unavailable page, this will be placed after unavailable page (...). Default is 1 space.</td>
</tr>
<tr>
<td>options.unavailable_tag_open</td>
<td><code>string</code></td>
<td>The unavailable page tag open. If you set to display unavailable page, this will be placed before unavailable page (...). Default is 1 space.</td>
</tr>
<tr>
<td>options.unavailable_after</td>
<td><code>number</code> | <code>false</code></td>
<td>Number of pages to display after last unavailable page. Set number as integer or set to false to not display the pages after unavailable. Default is 2.</td>
</tr>
<tr>
<td>options.unavailable_before</td>
<td><code>number</code> | <code>false</code></td>
<td>Number of pages to display before first unavailable page. Set number as integer or set to false to not display the pages before unavailable. Default is 2.</td>
</tr>
<tr>
<td>options.events</td>
<td><code>object</code></td>
<td>The events to be called.</td>
</tr>
<tr>
<td>options.events.onclick</td>
<td><code>function</code></td>
<td>On click event. This event is fired after displayed pagination. Set callback function here to call when user clicked. If you set callback function, this class will not use <code>preventDefault()</code> to let you handle it.</td>
</tr>
</tbody>
</table>
</div>
<p>The post <a href="https://rundiz.com/blog/downloads/pagination-js/">Pagination JS</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/downloads/pagination-js/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>ออกแบบ Input range</title>
		<link>https://rundiz.com/blog/articles/%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%81%e0%b8%9a%e0%b8%9a-input-range/</link>
					<comments>https://rundiz.com/blog/articles/%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%81%e0%b8%9a%e0%b8%9a-input-range/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Thu, 22 May 2025 14:51:13 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[css3]]></category>
		<category><![CDATA[html5]]></category>
		<category><![CDATA[input range]]></category>
		<category><![CDATA[javascript]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=1027</guid>

					<description><![CDATA[<p>Input range นั้นในปัจจุบันยังไม่มีชุดคำสั่งของ CSS ที่เ &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%81%e0%b8%9a%e0%b8%9a-input-range/">ออกแบบ Input range</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>Input range นั้นในปัจจุบัน<a href="https://github.com/w3c/csswg-drafts/issues/4410" target="_blank" rel="noopener">ยังไม่มีชุดคำสั่งของ CSS ที่เป็นมาตรฐานออกมาให้ใช้</a> มีเพียงแต่ที่เป็นของแต่ละเว็บเบราว์เซอร์เท่านั้น ทำให้การออกแบบยังค่อนข้างยาก และต้องเขียนซ้ำซ้อน แต่ก็ยังสามารถทำได้. โดยในบทความนี้จะออกแบบให้รองรับ engine ของเบราว์เซอร์หลักแค่ 2 เจ้าเท่านั้น คือ Gecko (Mozilla Firefox), และ Webkit (Google Chrome, Opera, Microsoft Edge).</p>
<h2>ส่วนประกอบของ input range</h2>
<figure class="figure d-block"><a href="https://rundiz.com/wp-content/uploads/2025/05/input-range-parts.webp"><img loading="lazy" decoding="async" class="aligncenter img-fluid wp-image-1029 size-large border" src="https://rundiz.com/wp-content/uploads/2025/05/input-range-parts-1024x312.webp" alt="" width="1024" height="312" srcset="https://rundiz.com/wp-content/uploads/2025/05/input-range-parts-1024x312.webp 1024w, https://rundiz.com/wp-content/uploads/2025/05/input-range-parts-300x92.webp 300w, https://rundiz.com/wp-content/uploads/2025/05/input-range-parts-768x234.webp 768w, https://rundiz.com/wp-content/uploads/2025/05/input-range-parts-1536x469.webp 1536w, https://rundiz.com/wp-content/uploads/2025/05/input-range-parts.webp 1655w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><figcaption class="figure-caption text-center">ส่วนประกอบต่างๆของ input range</figcaption></figure>
<p>input range นั้นประกอบด้วยส่วนต่างๆ คือ ตัวปุ่มเลื่อนหรือ thumb, ตัวรางหรือ track, ตัวระดับที่เลือกหรือ range progress.</p>
<h3>CSS สำหรับส่วนประกอบต่างๆ</h3>
<p>ผู้อ่านสามารถคลิกที่ชื่อโค้ดเพื่อตามไปอ่านเอกสารอ้างอิงได้บนเว็บ MDN.</p>
<h4>track</h4>
<p>ส่วนรางหรือ track นั้นจะใช้ CSS pseudo-element <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::-moz-range-track" target="_blank" rel="noopener"><code class="rd-syntax-highlighter language-css">::-moz-range-track</code></a> สำหรับ Gecko, และ <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-slider-runnable-track" target="_blank" rel="noopener"><code class="rd-syntax-highlighter language-css">::-webkit-slider-runnable-track</code></a> สำหรับ Webkit. สำหรับคำสั่งที่เป็นมาตรฐานกลางนั้น ยังไม่มีในขณะนี้.</p>
<h4>thumb</h4>
<p>ส่วนปุ่มเลื่อนหรือ thumb นั้นจะใช้ <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::-moz-range-thumb" target="_blank" rel="noopener"><code class="rd-syntax-highlighter language-css">::-moz-range-thumb</code></a> สำหรับ Gecko, และ <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-slider-thumb" target="_blank" rel="noopener"><code class="rd-syntax-highlighter language-css">::-webkit-slider-thumb</code></a> สำหรับ Webkit. สำหรับคำสั่งที่เป็นมาตรฐานกลางนั้นยังไม่มี.</p>
<h4>range progress</h4>
<p>ส่วนที่แสดงระดับที่เลือกหรือ range progress นั้นจะใช้ <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::-moz-range-progress" target="_blank" rel="noopener"><code class="rd-syntax-highlighter language-css">::-moz-range-progress</code></a> สำหรับ Gocko, ส่วนของ Webkit นั้นยังไม่มีตัวเลือกนี้ให้ใช้ในขณะนี้. สำหรับคำสั่งที่เป็นมาตรฐานกลางนั้นยังไม่มี.</p>
<h2>ออกแบบ input range</h2>
<p>การออกแบบ input range นั้นเบื้องต้นจะแยกออกเป็น 2 ส่วน คือ HTML และ CSS.</p>
<h4>HTML</h4>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-markup">&lt;p&gt;&lt;label for="cdex1"&gt;Basic&lt;/label&gt;
    &lt;input id="cdex1" class="rd-input-range custom-input-range-fixed-width" type="range"&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdex2"&gt;With datalist&lt;/label&gt;
    &lt;input id="cdex2" class="rd-input-range custom-input-range-fixed-width" type="range" list="dtl1"&gt;
    &lt;datalist id="dtl1"&gt;
        &lt;option value="0"&gt;&lt;/option&gt;
        &lt;option value="50"&gt;&lt;/option&gt;
        &lt;option value="100"&gt;&lt;/option&gt;
    &lt;/datalist&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdex3"&gt;Disabled&lt;/label&gt;
    &lt;input id="cdex3" class="rd-input-range custom-input-range-fixed-width" type="range" list="dtl1" disabled&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdex4"&gt;With datalist less than input max (500)&lt;/label&gt;
    &lt;input id="cdex4" class="rd-input-range custom-input-range-fixed-width" type="range" list="dtl2" min="0" max="500"&gt;
    &lt;datalist id="dtl2"&gt;
        &lt;option value="0"&gt;&lt;/option&gt;
        &lt;option value="50"&gt;&lt;/option&gt;
        &lt;option value="100"&gt;&lt;/option&gt;
    &lt;/datalist&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdex5"&gt;With datalist more than input max (100)&lt;/label&gt;
    &lt;input id="cdex5" class="rd-input-range custom-input-range-fixed-width" type="range" list="dtl3" min="0" max="100"&gt;
    &lt;datalist id="dtl3"&gt;
        &lt;option value="0"&gt;&lt;/option&gt;
        &lt;option value="50"&gt;&lt;/option&gt;
        &lt;option value="500"&gt;&lt;/option&gt;
    &lt;/datalist&gt;
&lt;/p&gt;</code></pre>
<h4>CSS</h4>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-css">input[type="range"].rd-input-range {
    -webkit-appearance: none;
    appearance: none;
    height: 37px;
    margin: 0;
    -webkit-tap-highlight-color: rgba(255, 255, 255, 0);/* remove white-blue background color on tap or on touch in chrome for android. */
    width: 100%;
}
input[type="range"].rd-input-range:disabled {
    /* this covered beginning part but not for thumb and track at the end. the last part of track will be covered by track selector */
    cursor: not-allowed;
    opacity: 0.5;
}

/* Input range's thumb */
input[type="range"].rd-input-range::-moz-range-thumb {
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 0px;
    box-shadow: none;
    cursor: pointer;
    height: 30px;
    width: 15px;
}
input[type="range"].rd-input-range:active:not(:disabled)::-moz-range-thumb {
    outline: 3px solid rgba(255, 100, 0, 0.3);
}
input[type="range"].rd-input-range:disabled::-moz-range-thumb {
    cursor: not-allowed;
}
input[type="range"].rd-input-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 0px;
    box-shadow: none;
    cursor: pointer;
    height: 30px;
    margin-top: -14px;
    width: 15px;
}
input[type="range"].rd-input-range:active:not(:disabled)::-webkit-slider-thumb {
    outline: 3px solid rgba(255, 100, 0, 0.3);
}
input[type="range"].rd-input-range:disabled::-webkit-slider-thumb {
    cursor: not-allowed;
}
/* End input range's thumb */

/* Input range track (can't use comma separate for ::-moz-xxx and ::-webkit-xxx) */
input[type="range"].rd-input-range::-moz-range-track {
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 0px;
    box-shadow: none;
    cursor: pointer;
    height: 3px;
}
input[type="range"].rd-input-range:disabled::-moz-range-track {
    cursor: not-allowed;
}
input[type="range"].rd-input-range::-webkit-slider-runnable-track {
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 0px;
    box-shadow: none;
    cursor: pointer;
    height: 3px;
}
input[type="range"].rd-input-range:disabled::-webkit-slider-runnable-track {
    cursor: not-allowed;
}
input[type="range"].rd-input-range:focus::-webkit-slider-runnable-track {
    background: #eee;
}
/* End input range track */

/* Input range progress */
input[type="range"].rd-input-range::-moz-range-progress {
    background-color: #52A0E5;
    border: 1px solid #aaa;
    border-radius: 0px;
    cursor: pointer;
    height: 3px;
}
/* currently not available for Webkit engine such as Chrome, Edge, etc. */
/* End input range progress */</code></pre>
<h4>ผลลัพธ์</h4>
<figure class="figure d-block"><a href="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-01.webp"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1030 img-fluid border" src="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-01.webp" alt="" width="270" height="374" srcset="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-01.webp 270w, https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-01-217x300.webp 217w" sizes="auto, (max-width: 270px) 100vw, 270px" /></a><figcaption class="figure-caption text-center">ตัวอย่างแรก ไม่ปรากฏขีดติ๊กสำหรับ datalist</figcaption></figure>
<p>ผลลัพธ์ที่เห็นคือเราจะสามารถเปลี่ยนรูปแบบ input range ได้แล้ว แต่ว่าจะยังไม่สามารถแสดงขีดติ๊กสำหรับ datalist ได้. เมื่อเทียบเคียงกับรูปแบบดั้งเดิมของบนเบราว์เซอร์ Firefox จะเป็นดังรูปด้านล่างนี้.</p>
<figure class="figure d-block"><a href="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-02.webp"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1031 img-fluid border" src="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-02.webp" alt="" width="299" height="304" srcset="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-02.webp 299w, https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-02-295x300.webp 295w" sizes="auto, (max-width: 299px) 100vw, 299px" /></a><figcaption class="figure-caption text-center">รูปแบบเดิมๆบน Firefox</figcaption></figure>
<p>รูปแบบเดิมๆบนเบราว์เซอร์นั้น จะพบว่ามีขีดติ๊กสำหรับ datalist ปรากฏอยู่.</p>
<h3>ออกแบบ datalist</h3>
<p>การออกแบบ datalist หรือขีดติ๊กนั้น จะต้องมีการใช้ JavaScript เพิ่มเติม เนื่องจากไม่มีคำสั่งใดๆสำหรับ CSS ที่รองรับได้โดยสมบูรณ์เลย. แม้จะสามารถใช้ display flex ได้แต่ก็จะไม่ได้ผลเมื่อ datalist มี option ที่ไม่ได้เป็นระยะที่เท่ากัน หรือไม่สมมาตรกัน ดังตัวอย่าง (คือไม่ใช่ 0, 50, 100 ประมาณนี้ แต่จะเป็น 0, 10, 20 แค่นี้ก็ได้).</p>
<h4>HTML</h4>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-markup">&lt;p&gt;&lt;label for="cdirdex1"&gt;With datalist&lt;/label&gt;
    &lt;div class="rd-input-range-with-datalist"&gt;
        &lt;input id="cdirdex1" class="rd-input-range" type="range" list="cdirddtl1"&gt;
        &lt;datalist id="cdirddtl1"&gt;
            &lt;option value="0"&gt;&lt;/option&gt;
            &lt;option value="50"&gt;&lt;/option&gt;
            &lt;option value="100"&gt;&lt;/option&gt;
        &lt;/datalist&gt;
    &lt;/div&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdirdex4"&gt;With datalist less than input max (500)&lt;/label&gt;
    &lt;div class="rd-input-range-with-datalist"&gt;
        &lt;input id="cdirdex4" class="rd-input-range" type="range" list="cdirddtl2" min="0" max="500"&gt;
        &lt;datalist id="cdirddtl2"&gt;
            &lt;option value="0"&gt;&lt;/option&gt;
            &lt;option value="50"&gt;&lt;/option&gt;
            &lt;option value="100"&gt;&lt;/option&gt;
        &lt;/datalist&gt;
    &lt;/div&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdirdex5"&gt;With datalist more than input max (100) and label&lt;/label&gt;
    &lt;div class="rd-input-range-with-datalist"&gt;
        &lt;input id="cdirdex5" class="rd-input-range" type="range" list="cdirddtl3" min="0" max="100"&gt;
        &lt;datalist id="cdirddtl3"&gt;
            &lt;option value="0" label="0"&gt;&lt;/option&gt;
            &lt;option value="25" label="25"&gt;&lt;/option&gt;
            &lt;option value="50" label="50"&gt;&lt;/option&gt;
            &lt;option value="500" label="500"&gt;&lt;/option&gt;
        &lt;/datalist&gt;
    &lt;/div&gt;
&lt;/p&gt;</code></pre>
<h4>CSS</h4>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-css">/* Custom design of input range tick marks (datalist) */
.rd-input-range-with-datalist {
    display: block;
}

.rd-input-range-with-datalist .rd-input-range-custom-ticks-container {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    margin-left: auto;
    margin-right: auto;
    position: relative;
    width: calc(100% - (15px));/* 15 px is input range thumb size. refer from typo-and-form/_form-input-range.css file. */
}

.rd-input-range-with-datalist .rd-input-range-custom-ticks-container &gt; .rd-input-range-custom-tick {
    color: #777;
    font-size: small;
    font-weight: lighter;
    padding: 0;
    position: absolute;
}
.rd-input-range-with-datalist .rd-input-range-custom-ticks-container &gt; .rd-input-range-custom-tick::before {
    border-left: 1px solid #aaa;
    content: "";
    display: block;
    min-height: 10px;
}
/* End custom design of input range tick marks. */</code></pre>
<h4>JavaScript</h4>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-javascript">let isListenedInputRangeEvent = false;

/**
 * Custom input range with `datalist`
 *
 * @link https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/range#validation Document reference for default `max` value.
 */
function customInputRangeWithDatalist() {
    const customIRWDClass = "rd-input-range-with-datalist";
    const customTicksContainerClass = "rd-input-range-custom-ticks-container";
    const customTickClass = "rd-input-range-custom-tick";

    // setup tick marks.
    document
        .querySelectorAll("." + customIRWDClass)
        ?.forEach((eachCustomRange) =&gt; {
            const eachDatalist = eachCustomRange.querySelector("datalist");
            // get input `max` attribute value. default is 100.
            let inputRangeMax = 100;
            const inputRange = eachCustomRange.querySelector(
                'input[type="range"]'
            );
            if (inputRange.hasAttribute("max")) {
                inputRangeMax = inputRange.getAttribute("max");
            }

            if (
                eachCustomRange.querySelector("." + customTicksContainerClass)
            ) {
                // if there is already generated ticks.
                // remove because we will re-generate.
                eachCustomRange
                    .querySelector("." + customTicksContainerClass)
                    .remove();
            }

            // generate custom ticks.
            // this is for fix that `&lt;option&gt;` tag can't position with CSS.
            let customTicksHTML =
                '&lt;div class="' + customTicksContainerClass + '"&gt;';
            for (let i = 0; i &lt; eachDatalist.options.length; i++) {
                const optionValue = parseFloat(eachDatalist?.options[i]?.value);
                customTicksHTML += '&lt;div class="' + customTickClass + '"';
                customTicksHTML +=
                    ' data-value="' +
                    (eachDatalist?.options[i]?.value ? optionValue : "") +
                    '"';
                customTicksHTML +=
                    ' data-label="' +
                    (eachDatalist?.options[i]?.label
                        ? eachDatalist.options[i].label
                        : "") +
                    '"';
                customTicksHTML += ' style="';
                if (optionValue &lt;= inputRangeMax) {
                    // if this option is not more than input range's max value.
                    customTicksHTML +=
                        "left: " +
                        (100 * parseFloat(optionValue)) /
                            parseFloat(inputRangeMax) +
                        "%;";
                } else {
                    // if this option is more than input range's max value.
                    // hide it.
                    customTicksHTML += "display: none;";
                }
                customTicksHTML += '"'; // close style attribute.
                customTicksHTML += "&gt;"; // close opened tag.
                if (eachDatalist?.options[i]?.label) {
                    customTicksHTML += eachDatalist.options[i].label;
                }
                customTicksHTML += "&lt;/div&gt;"; // close each custom tick.
            } // endfor; loop datalist's options.
            customTicksHTML += "&lt;/div&gt;";
            eachDatalist.insertAdjacentHTML("afterend", customTicksHTML);

            // get max height of each tick and set the heighest number to the container.
            // this is for fixing position absolute cause the container has height zero.
            let maxHeightTick = 0;
            eachCustomRange
                .querySelectorAll("." + customTickClass)
                ?.forEach((eachTick) =&gt; {
                    const eachTickRect = eachTick.getBoundingClientRect();
                    if (eachTickRect.height &gt; maxHeightTick) {
                        maxHeightTick = eachTickRect.height;
                    }
                });
            eachCustomRange.querySelector(
                "." + customTicksContainerClass
            ).style.height = maxHeightTick + "px";
            // do not automatically append the `output` HTML element to let dev design their style.
        });
    // end setup tick marks.

    if (false === isListenedInputRangeEvent) {
        // if not listened the input range event yet.
        // listen on slide the input range and show its value to `output` HTML element.
        document.addEventListener("input", (event) =&gt; {
            isListenedInputRangeEvent = true;
            let thisInput = event.target;
            if (!thisInput.closest("." + customIRWDClass)) {
                return;
            }
            const customIRWDContainer = thisInput.closest(
                "." + customIRWDClass
            );
            const outputHTML = customIRWDContainer.querySelector("output");
            if (outputHTML) {
                // if there is `output` HTML in the custom input range.
                outputHTML.value = thisInput.value;
            }
        });
    }
} // customInputRangeWithDatalist


// to use: call the function.
customInputRangeWithDatalist();</code></pre>
<h4>ผลลัพธ์</h4>
<figure class="figure d-block"><a href="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-03.webp"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-1032 img-fluid border" src="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-03.webp" alt="" width="375" height="275" srcset="https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-03.webp 375w, https://rundiz.com/wp-content/uploads/2025/05/custom-input-range-sample-03-300x220.webp 300w" sizes="auto, (max-width: 375px) 100vw, 375px" /></a><figcaption class="figure-caption text-center">ตัวอย่างที่สอง ปรากฏขีดติ๊กสำหรับ datalist แล้ว</figcaption></figure>
<p>จากผลลัพธ์จะปรากฏขีดติ๊กสำหรับ datalist แล้ว</p>
<h3>โค้ดทั้งหมด</h3>
<p>เมื่อนำโค้ดทั้งหมดมารวมกันแล้วแยกเป็น HTML, CSS โดยเว้น JavaScript เอาไว้เนื่องจากโค้ดที่เผยแพร่ไว้แล้วด้านบนมีครบถ้วนหมดแล้ว ก็จะได้ดังต่อไปนี้.</p>
<p>HTML</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-markup">&lt;h2&gt;Custom design of input range&lt;/h2&gt;
&lt;p&gt;&lt;label for="cdex1"&gt;Basic&lt;/label&gt;
    &lt;input id="cdex1" class="rd-input-range custom-input-range-fixed-width" type="range"&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdex2"&gt;With datalist&lt;/label&gt;
    &lt;input id="cdex2" class="rd-input-range custom-input-range-fixed-width" type="range" list="dtl1"&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdex3"&gt;Disabled&lt;/label&gt;
    &lt;input id="cdex3" class="rd-input-range custom-input-range-fixed-width" type="range" list="dtl1" disabled&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdex4"&gt;With datalist less than input max (500)&lt;/label&gt;
    &lt;input id="cdex4" class="rd-input-range custom-input-range-fixed-width" type="range" list="dtl2" min="0" max="500"&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdex5"&gt;With datalist more than input max (100)&lt;/label&gt;
    &lt;input id="cdex5" class="rd-input-range custom-input-range-fixed-width" type="range" list="dtl3" min="0" max="100"&gt;
&lt;/p&gt;

&lt;hr&gt;

&lt;h2&gt;Custom design of input range and datalist tick marks&lt;/h2&gt;
&lt;p&gt;This part required JS and custom HTML to work.&lt;/p&gt;
&lt;p&gt;&lt;label for="cdirdex1"&gt;With datalist&lt;/label&gt;
    &lt;div class="rd-input-range-with-datalist"&gt;
        &lt;input id="cdirdex1" class="rd-input-range" type="range" list="cdirddtl1"&gt;
        &lt;datalist id="cdirddtl1"&gt;
            &lt;option value="0"&gt;&lt;/option&gt;
            &lt;option value="50"&gt;&lt;/option&gt;
            &lt;option value="100"&gt;&lt;/option&gt;
        &lt;/datalist&gt;
    &lt;/div&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdirdex4"&gt;With datalist less than input max (500)&lt;/label&gt;
    &lt;div class="rd-input-range-with-datalist"&gt;
        &lt;input id="cdirdex4" class="rd-input-range" type="range" list="cdirddtl2" min="0" max="500"&gt;
        &lt;datalist id="cdirddtl2"&gt;
            &lt;option value="0"&gt;&lt;/option&gt;
            &lt;option value="50"&gt;&lt;/option&gt;
            &lt;option value="100"&gt;&lt;/option&gt;
        &lt;/datalist&gt;
    &lt;/div&gt;
&lt;/p&gt;
&lt;p&gt;&lt;label for="cdirdex5"&gt;With datalist more than input max (100) and label&lt;/label&gt;
    &lt;div class="rd-input-range-with-datalist"&gt;
        &lt;input id="cdirdex5" class="rd-input-range" type="range" list="cdirddtl3" min="0" max="100"&gt;
        &lt;datalist id="cdirddtl3"&gt;
            &lt;option value="0" label="0"&gt;&lt;/option&gt;
            &lt;option value="25" label="25"&gt;&lt;/option&gt;
            &lt;option value="50" label="50"&gt;&lt;/option&gt;
            &lt;option value="500" label="500"&gt;&lt;/option&gt;
        &lt;/datalist&gt;
    &lt;/div&gt;
&lt;/p&gt;</code></pre>
<p>CSS</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-css">/* Custom design input range. ============================== */
input[type="range"].rd-input-range {
    -webkit-appearance: none;
    appearance: none;
    height: 37px;
    margin: 0;
    -webkit-tap-highlight-color: rgba(255, 255, 255, 0);/* remove white-blue background color on tap or on touch in chrome for android. */
    width: 100%;
}
input[type="range"].rd-input-range:disabled {
    /* this covered beginning part but not for thumb and track at the end. the last part of track will be covered by track selector */
    cursor: not-allowed;
    opacity: 0.5;
}

/* Input range's thumb */
input[type="range"].rd-input-range::-moz-range-thumb {
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 0px;
    box-shadow: none;
    cursor: pointer;
    height: 30px;
    width: 15px;
}
input[type="range"].rd-input-range:active:not(:disabled)::-moz-range-thumb {
    outline: 3px solid rgba(255, 100, 0, 0.3);
}
input[type="range"].rd-input-range:disabled::-moz-range-thumb {
    cursor: not-allowed;
}
input[type="range"].rd-input-range::-webkit-slider-thumb {
    -webkit-appearance: none;
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 0px;
    box-shadow: none;
    cursor: pointer;
    height: 30px;
    margin-top: -14px;
    width: 15px;
}
input[type="range"].rd-input-range:active:not(:disabled)::-webkit-slider-thumb {
    outline: 3px solid rgba(255, 100, 0, 0.3);
}
input[type="range"].rd-input-range:disabled::-webkit-slider-thumb {
    cursor: not-allowed;
}
/* End input range's thumb */

/* Input range track (can't use comma separate for ::-moz-xxx and ::-webkit-xxx) */
input[type="range"].rd-input-range::-moz-range-track {
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 0px;
    box-shadow: none;
    cursor: pointer;
    height: 3px;
}
input[type="range"].rd-input-range:disabled::-moz-range-track {
    cursor: not-allowed;
}
input[type="range"].rd-input-range::-webkit-slider-runnable-track {
    background: #eee;
    border: 1px solid #aaa;
    border-radius: 0px;
    box-shadow: none;
    cursor: pointer;
    height: 3px;
}
input[type="range"].rd-input-range:disabled::-webkit-slider-runnable-track {
    cursor: not-allowed;
}
input[type="range"].rd-input-range:focus::-webkit-slider-runnable-track {
    background: #eee;
}
/* End input range track */

/* Input range progress */
input[type="range"].rd-input-range::-moz-range-progress {
    background-color: #52A0E5;
    border: 1px solid #aaa;
    border-radius: 0px;
    cursor: pointer;
    height: 3px;
}
/* currently not available for Webkit engine such as Chrome, Edge, etc. */
/* End input range progress */

/* End custom design of input range. ===================== */


/* Custom design of input range tick marks (datalist) */
.rd-input-range-with-datalist {
    display: block;
}

.rd-input-range-with-datalist .rd-input-range-custom-ticks-container {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    margin-left: auto;
    margin-right: auto;
    position: relative;
    width: calc(100% - (15px));/* 15 px is input range thumb size. refer from typo-and-form/_form-input-range.css file. */
}

.rd-input-range-with-datalist .rd-input-range-custom-ticks-container &gt; .rd-input-range-custom-tick {
    color: #777;
    font-size: small;
    font-weight: lighter;
    padding: 0;
    position: absolute;
}
.rd-input-range-with-datalist .rd-input-range-custom-ticks-container &gt; .rd-input-range-custom-tick::before {
    border-left: 1px solid #aaa;
    content: "";
    display: block;
    min-height: 10px;
}
/* End custom design of input range tick marks. */</code></pre>
<h4>ตัวอย่างการทำงานจริง</h4>
<p>คุณผู้อ่านสามารถทดลองดูตัวอย่างจริงได้บน codepen ที่ได้ทำไว้แล้วและนำมาเผยแพร่ไว้ด้านล่างนี้.<br />
<iframe style="width: 100%;" title="Custom input range &amp; tick marks" src="https://codepen.io/ve3/embed/xbbvEeM?default-tab=html%2Cresult" height="300" frameborder="no" scrolling="no" allowfullscreen="allowfullscreen"><br />
See the Pen <a href="https://codepen.io/ve3/pen/xbbvEeM"><br />
Custom input range &amp; tick marks</a> by vee (<a href="https://codepen.io/ve3">@ve3</a>)<br />
on <a href="https://codepen.io">CodePen</a>.<br />
</iframe></p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%81%e0%b8%9a%e0%b8%9a-input-range/">ออกแบบ Input range</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%81%e0%b8%9a%e0%b8%9a-input-range/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>สั่งให้เว็บพูดออกเสียงได้ด้วย Web Speech API</title>
		<link>https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%b1%e0%b9%88%e0%b8%87%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%a7%e0%b9%87%e0%b8%9a%e0%b8%9e%e0%b8%b9%e0%b8%94%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87/</link>
					<comments>https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%b1%e0%b9%88%e0%b8%87%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%a7%e0%b9%87%e0%b8%9a%e0%b8%9e%e0%b8%b9%e0%b8%94%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Mon, 31 Mar 2025 18:03:58 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[javascript]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=1014</guid>

					<description><![CDATA[<p>ภาษา JavaScript ที่เราใช้กันอยู่นั้นมีส่วนเชื่อมต่อหนึ่ &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%b1%e0%b9%88%e0%b8%87%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%a7%e0%b9%87%e0%b8%9a%e0%b8%9e%e0%b8%b9%e0%b8%94%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87/">สั่งให้เว็บพูดออกเสียงได้ด้วย Web Speech API</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>ภาษา JavaScript ที่เราใช้กันอยู่นั้นมีส่วนเชื่อมต่อหนึ่งที่ช่วยให้หน้าเว็บหรือโปรแกรมที่เราเขียนสามารถอ่านออกเสียงได้ นั่นคือ <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Speech_API" target="_blank" rel="noopener nofollow">Web Speech API</a>. สำหรับเว็บเบราว์เซอร์ที่รองรับนั้นก็รองรับเป็นส่วนมากแล้วสำหรับโค้ดที่สั่งให้ออกเสียงจากตัวหนังสือ อาทิเช่น Google Chrome, Firefox, Edge, Opera. สำหรับคำสั่งที่ให้ออกเสียงจากตัวหนังสือนั้น โดยหลักๆเราจะใช้แค่ <a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis" target="_blank" rel="noopener nofollow"><code class="rd-syntax-highlighter language-javascript">window.speechSynthesis</code></a> กับ <a href="https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance/SpeechSynthesisUtterance" target="_blank" rel="noopener nofollow"><code class="rd-syntax-highlighter language-javascript">SpeechSynthesisUtterance()</code></a> เท่านั้น ดังตัวอย่างต่อไปนี้.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-javascript">const synth = window.speechSynthesis;
const utterance = new SpeechSynthesisUtterance('hello');
synth.speak(utterance);</code></pre>
<p>เมื่อโหลดหน้าเว็บที่มีโค้ดด้านบนนี้ เบราว์เซอร์ก็จะส่งเสียง hello ออกมา. แต่ถ้าเราเปลี่ยนจากคำว่า <em>hello</em> เป็นภาษาไทย เช่น <em>สวัสดี</em> เป็นต้น ก็จะอาจจะไม่มีเสียงใดๆเนื่องจากระบบและค่าเริ่มต้นของภาษานั้นไม่รองรับ ให้เราทำการเพิ่ม <code class="rd-syntax-highlighter language-javascript">utterance.lang = 'th-TH';</code> ก็จะสามารถอ่านภาษาไทยได้แล้ว.</p>
<p>สำหรับการตั้งค่าอื่นๆ เรายังกำหนดได้อีก ผ่าน properties ต่างๆ เช่น เสียงที่จะใช้ (<code class="rd-syntax-highlighter language-javascript">voice</code>), ความสูงต่ำของเสียง (<code class="rd-syntax-highlighter language-javascript">pitch</code>), ความเร็วในการพูด (<code class="rd-syntax-highlighter language-javascript">rate</code>), ความดัง (<code class="rd-syntax-highlighter language-javascript">volume</code>) ทั้งนี้ขอให้คุณผู้อ่านคลิกดูเอกสารอ้างอิงจากลิ้งค์ในชื่อ class ที่ระบุด้านบนของบทความ. คุณผู้อ่านสามารถเข้าไปทดลองตัวอย่างที่ผมได้เขียนไว้แล้วได้ที่ <a href="https://jsfiddle.net/y7eLp8g4/" target="_blank" rel="noopener nofollow">https://jsfiddle.net/y7eLp8g4/</a> หรือ <a href="https://codepen.io/ve3/pen/MYWzEwg" target="_blank" rel="noopener nofollow">https://codepen.io/ve3/pen/MYWzEwg</a>.</p>
<h3>ตัวอย่าง</h3>
<p><iframe style="width: 100%;" title="Untitled" src="https://codepen.io/ve3/embed/MYWzEwg?default-tab=html%2Cresult" height="300" frameborder="no" scrolling="no" allowfullscreen="allowfullscreen"><br />
See the Pen <a href="https://codepen.io/ve3/pen/MYWzEwg"><br />
Untitled</a> by vee (<a href="https://codepen.io/ve3">@ve3</a>)<br />
on <a href="https://codepen.io">CodePen</a>.<br />
</iframe></p>
<h3>ปัญหา</h3>
<p>การไม่สามารถอ่านตัวเลขที่มีจำนวนเยอะๆได้ เช่นเลข 123455669 จะไม่สามารถอ่านออกมาเป็นจำนวนร้อยล้านได้เลย ทั้งภาษาไทยและอังกฤษ จากการทดสอบบน Windows Firefox, Windows Edge. แต่มันจะสามารถอ่านได้ถ้าหากเพิ่มเครื่องหมาย , ให้ทุกๆ 3 หลัก เช่นเลข 123,455,669 เป็นต้น. ดังนั้นในส่วนนี้หากต้องการให้อ่านออกมาเป็นแบบใด จำเป็นจะต้องเขียนโค้ดเพิ่มเติมในการตรวจสอบและแก้ไขก่อนส่งให้ตัวโปรแกรมทำการอ่านในลำดับต่อมา.</p>
<p>การอ่านออกเสียงไม่ตรงภาษาของค่าเริ่มต้น. จากการทดสอบบน Android Google Chrome พบว่า เมื่อเขียนโค้ดที่พื้นฐานที่สุด แล้วเรียกใช้ property <code class="rd-syntax-highlighter language-javascript">lang</code> เข้าไป สลับกับไม่ใช้ พบว่าบางครั้งตัวโปรแกรมจะค้างเอาค่า property <code class="rd-syntax-highlighter language-javascript">lang</code> ของเก่ามาใช้ ทำให้การออกเสียงไม่ตรงค่าเริ่มต้นหรือเป็นไปตามที่ต้องการ. ทางแก้ไขคือให้กำหนดค่า property <code class="rd-syntax-highlighter language-javascript">lang</code> ทุกครั้ง.</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%b1%e0%b9%88%e0%b8%87%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%a7%e0%b9%87%e0%b8%9a%e0%b8%9e%e0%b8%b9%e0%b8%94%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87/">สั่งให้เว็บพูดออกเสียงได้ด้วย Web Speech API</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%b1%e0%b9%88%e0%b8%87%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%a7%e0%b9%87%e0%b8%9a%e0%b8%9e%e0%b8%b9%e0%b8%94%e0%b8%ad%e0%b8%ad%e0%b8%81%e0%b9%80%e0%b8%aa%e0%b8%b5%e0%b8%a2%e0%b8%87/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>แก้สระลอยภาษาไทยใน PHP PDF</title>
		<link>https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%81%e0%b9%89%e0%b8%aa%e0%b8%a3%e0%b8%b0%e0%b8%a5%e0%b8%ad%e0%b8%a2%e0%b8%a0%e0%b8%b2%e0%b8%a9%e0%b8%b2%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99-php-pdf/</link>
					<comments>https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%81%e0%b9%89%e0%b8%aa%e0%b8%a3%e0%b8%b0%e0%b8%a5%e0%b8%ad%e0%b8%a2%e0%b8%a0%e0%b8%b2%e0%b8%a9%e0%b8%b2%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99-php-pdf/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Thu, 27 Feb 2025 01:14:43 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[pdf]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=1006</guid>

					<description><![CDATA[<p>ผู้เขียน PHP ที่ต้องการสร้างเอกสาร PDF อาจพบเจอปัญหาที่ &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%81%e0%b9%89%e0%b8%aa%e0%b8%a3%e0%b8%b0%e0%b8%a5%e0%b8%ad%e0%b8%a2%e0%b8%a0%e0%b8%b2%e0%b8%a9%e0%b8%b2%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99-php-pdf/">แก้สระลอยภาษาไทยใน PHP PDF</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>ผู้เขียน PHP ที่ต้องการสร้างเอกสาร PDF อาจพบเจอปัญหาที่แก้ได้ยากบ่อยครั้งซึ่งเป็นปัญหายอดนิยม นั่นคือเสียงวรรณยุกต์ลอย, หรือสระลอย. ปัญหาดังกล่าวเกิดจากฟอนต์เป็นหลัก เพราะฟอนต์บางตัวก็ทำงานได้ดี แต่บางตัวก็ไม่เป็นเช่นนั้น. การแก้สระลอยในโค้ดต่อไปนี้อาจแก้ได้เฉพาะที่มีเสียงวรรณยุกต์ลอยเท่านั้น (เช่น ไม้เอก, ไม้โท) แต่ไม่สามารถแก้เสียงวรรณยุกต์จม, หรือสระจมได้ เนื่องจากเป็นปัญหาที่ฟอนต์นั้นๆไม่ได้ออกแบบมาให้มีอักขระในส่วนนี้เลย จึงไม่สามารถนำมาใช้ทดแทนแก้ปัญหาได้.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-php">/**
 * Fix Thai vowels and characters. (สระลอย, เสียงวรรณยุกต์ลอย)
 *
 * @link https://gist.github.com/dtinth/716814 Original source code
 * @param string $text
 * @return string
 */
function fixThaiVowels(string $text): string
{
    // เสียงวรรณยุกต์ และทัณฑฆาต (การันต์)
    $back = [
        "\xE0\xB9\x88" =&gt; "\xEF\x9C\x8A", //่(ไม้เอก)
        "\xE0\xB9\x89" =&gt; "\xEF\x9C\x8B", //(ไม้โท)
        "\xE0\xB9\x8A" =&gt; "\xEF\x9C\x8C", //(ไม้ตรี)
        "\xE0\xB9\x8B" =&gt; "\xEF\x9C\x8D", //(ไม้จัตวา)
        "\xE0\xB9\x8C" =&gt; "\xEF\x9C\x8E" //(ตัวการันต์)
    ];
    $bottomVowels = [
        "\xE0\xB8\xB8" =&gt; "\xEF\x9C\x98", // สระอุ
        "\xE0\xB8\xB9" =&gt; "\xEF\x9C\x99", // สระอู
        "\xE0\xB8\xBA" =&gt; "\xEF\x9C\x9A", // พินธุ
    ];
    $replaces = [];
    // ตัวอักษรที่มีความสูงที่จะต้องไม่ให้ชนกับเสียงวรรณยุกต์
    $highChars = [
        "ิ", // สระอิ
        "ี", // สระอี
        "ึ", // สระอึ
        "ื", // สระอือ
        "ั", // ไม้หันอากาศ
        "ำ", // สระอำ
        "ํ", // สระอำ ที่ไม่มีสระอา
        "ป",
        "ฝ",
        "ฟ",
        "ฬ",
    ];
    // ตัวอักษรที่ลากลงด้านล่าง
    $lowChars = [
        "ฎ",
        "ฏ",
        "ฤ",
        "ฦ",
    ];
    // ตัวอักษรที่มีฐานด้านล่างและจะไปชนกับสระที่อยู่ด้านล่าง ซึ่งจะต้องถูกแทนที่โดยการตัดฐานล่างออกไป
    $lowReplaceChars = [
        'ญ' =&gt; "\xEF\x9C\x8F",
        'ฐ' =&gt; "\xEF\x9C\x80",
    ];

    // loop กำหนดค่าคืนกลับ (replace) เสียงวรรณยุกต์ที่จะต้องลอยสูง
    foreach ($highChars as $p) {
        if ($p !== 'ำ' &amp;&amp; $p !== 'ํ') {
            for ($i = 0x8A; $i &lt;= 0x8E; ++$i) {
                $from = $p . "\xEF\x9C" . chr($i);
                $to = $p . "\xE0\xB9" . chr($i - 2);
                $replaces[$from] = $to;
            }// endfor;
            unset($i);
        } else {
            for ($i = 0x8A; $i &lt;= 0x8E; ++$i) {
                $from = "\xEF\x9C" . chr($i) . $p;
                $to = "\xE0\xB9" . chr($i - 2) . $p;
                $replaces[$from] = $to;
            }// endfor;
            unset($i);
        }// endif;
    }// endforeach;
    unset($highChars, $p);

    // loop กำหนดค่าสระด้านล่าง ที่จะต้องเปลี่ยน (replace) เมื่อเจอตัวอักษรที่ลากลงด้านล่าง
    foreach ($lowChars as $p) {
        foreach ($bottomVowels as $orig =&gt; $replace) {
            $from = $p . $orig;
            $to = $p . $replace;
            $replaces[$from] = $to;
        }// endforeach;
        unset($orig, $replace);
    }// endforeach;
    unset($lowChars, $p);

    // loop กำหนดตัวอักษรที่มีฐานด้านล่าง ที่จะต้องเปลี่ยน (replace) เมื่อมีสระด้านล่างตามมาด้วย
    foreach ($lowReplaceChars as $origChar =&gt; $replaceChar) {
        foreach ($bottomVowels as $orig =&gt; $replace) {
            $from = $origChar . $orig;
            $to = $replaceChar . $orig;
            $replaces[$from] = $to;
        }// endforeach;
        unset($orig, $replace);
    }// endforeach;
    unset($lowReplaceChars, $origChar, $replaceChar);

    $text = strtr($text, $back);
	$text = strtr($text, $replaces);
    unset($back, $replaces);
    return $text;
}// fixThaiVowels</code></pre>
<p>ที่มาของโค้ดด้านบนนี้ แรกเริ่มเดิมทีมาจาก URL <a href="https://gist.github.com/dtinth/716814" target="_blank" rel="noopener">https://gist.github.com/dtinth/716814</a> แต่ได้มีการปรับแต่งเรื่อยมาจนกระทั่งผู้เขียนได้เพิ่มการแก้ปัญหา สระด้านล่างไปชนกับตัวอักษรที่ลากยาวลงมาด้านล่างด้วย เช่น <strong>ญ</strong>, <strong>ฎ</strong> เป็นต้น.</p>
<h2>ทดสอบ</h2>
<p>ในการทดสอบ ผู้เขียนจะใช้โค้ดของ mpdf ซึ่งคุณผู้อ่านจะนำไปใช้กับอะไรอย่างอื่นก็ได้ทั้งหมด. ตัวอย่างโค้ดทดสอบ.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-php">require 'vendor/autoload.php';

function mpdfGetDefaultFonts(): array
{
    $defaultConfig = (new \Mpdf\Config\ConfigVariables())-&gt;getDefaults();
    $fontDirs = $defaultConfig['fontDir'];
    $defaultFontConfig = (new \Mpdf\Config\FontVariables())-&gt;getDefaults();
    $fontData = $defaultFontConfig['fontdata'];
    // remove not exists font.
    unset($fontData['eeyekunicode']);
    unset($defaultConfig, $defaultFontConfig);

    return [
        'fontDirs' =&gt; $fontDirs,
        'fontData' =&gt; $fontData,
    ];
}

list('fontDirs' =&gt; $fontDirs, 'fontData' =&gt; $fontData) = mpdfGetDefaultFonts();
$fontDirs = array_merge($fontDirs, [__DIR__ . DIRECTORY_SEPARATOR . 'custom-fonts']);// แก้ไขของคุณ
$fontData = $fontData + [
    'sarabunnew' =&gt; [
        'R' =&gt; 'THSarabunNew.ttf',
    ],
];// แก้ไขของคุณ
$config = [
    'default_font' =&gt; 'sans-serif',
    'default_font_size' =&gt; 22,
    'fontdata' =&gt; $fontData,
    'fontDir' =&gt; $fontDirs,
];
unset($fontDirs);

$mpdf = new \Mpdf\Mpdf($config);
$fontsToTest = ['garuda', 'zawgyi-one', 'sarabunnew'];

$html = '&lt;!DOCTYPE html&gt;&lt;html&gt;' . PHP_EOL;
$html .= '&lt;head&gt;&lt;meta charset="utf-8"&gt;&lt;/head&gt;' . PHP_EOL;
$html .= '&lt;body&gt;' . PHP_EOL;
$html .= '&lt;h1&gt;Fix Thai vowels&lt;/h1&gt;' . PHP_EOL;
foreach ($fontsToTest as $font) {
    $html .= '&lt;h3&gt;Font: ' . $font . '&lt;/h3&gt;' . PHP_EOL;
    $html .= '&lt;p style="font-family:' . $font . ';"&gt;' . fixThaiVowels('อ่า อิ่ อี่ อึ่ อื่ อ้า อิ้ อี้ อึ้ อื้ อ๊า อิ๊ อี๊ อึ๊ อื๊ อ๋า อิ๋ อี๋ อึ๋ อื๋');
    $html .= '&lt;/p&gt;' . PHP_EOL;
}
$html .= '&lt;/body&gt;' . PHP_EOL;
$html .= '&lt;/html&gt;' . PHP_EOL;

$mpdf-&gt;WriteHTML($html);
$mpdf-&gt;Output();</code></pre>
<p>ผลลัพธ์ควรจะได้คล้ายภาพต่อไปนี้</p>
<p><a href="https://rundiz.com/wp-content/uploads/2025/02/fixed-thai-vowels-ss.png"><img loading="lazy" decoding="async" width="1024" height="469" class="aligncenter size-large wp-image-1008 img-fluid border" src="https://rundiz.com/wp-content/uploads/2025/02/fixed-thai-vowels-ss-1024x469.png" alt="screenshot of fixed Thai vowels" srcset="https://rundiz.com/wp-content/uploads/2025/02/fixed-thai-vowels-ss-1024x469.png 1024w, https://rundiz.com/wp-content/uploads/2025/02/fixed-thai-vowels-ss-300x137.png 300w, https://rundiz.com/wp-content/uploads/2025/02/fixed-thai-vowels-ss-768x352.png 768w, https://rundiz.com/wp-content/uploads/2025/02/fixed-thai-vowels-ss.png 1382w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a></p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%81%e0%b9%89%e0%b8%aa%e0%b8%a3%e0%b8%b0%e0%b8%a5%e0%b8%ad%e0%b8%a2%e0%b8%a0%e0%b8%b2%e0%b8%a9%e0%b8%b2%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99-php-pdf/">แก้สระลอยภาษาไทยใน PHP PDF</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%81%e0%b9%89%e0%b8%aa%e0%b8%a3%e0%b8%b0%e0%b8%a5%e0%b8%ad%e0%b8%a2%e0%b8%a0%e0%b8%b2%e0%b8%a9%e0%b8%b2%e0%b9%84%e0%b8%97%e0%b8%a2%e0%b9%83%e0%b8%99-php-pdf/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MySQL (MariaDB) กับขีดจำกัดการเชื่อมต่อสูงสุด</title>
		<link>https://rundiz.com/blog/articles/mysql-mariadb-%e0%b8%81%e0%b8%b1%e0%b8%9a%e0%b8%82%e0%b8%b5%e0%b8%94%e0%b8%88%e0%b8%b3%e0%b8%81%e0%b8%b1%e0%b8%94%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b9%80%e0%b8%8a%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%a1/</link>
					<comments>https://rundiz.com/blog/articles/mysql-mariadb-%e0%b8%81%e0%b8%b1%e0%b8%9a%e0%b8%82%e0%b8%b5%e0%b8%94%e0%b8%88%e0%b8%b3%e0%b8%81%e0%b8%b1%e0%b8%94%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b9%80%e0%b8%8a%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%a1/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Fri, 27 Dec 2024 08:40:07 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[mariadb]]></category>
		<category><![CDATA[mysql]]></category>
		<category><![CDATA[php]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=996</guid>

					<description><![CDATA[<p>บางครั้งผู้อ่านที่เป็นโปรแกรมเมอร์ได้เขียนโค้ดอย่างรอบค &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/mysql-mariadb-%e0%b8%81%e0%b8%b1%e0%b8%9a%e0%b8%82%e0%b8%b5%e0%b8%94%e0%b8%88%e0%b8%b3%e0%b8%81%e0%b8%b1%e0%b8%94%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b9%80%e0%b8%8a%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%a1/">MySQL (MariaDB) กับขีดจำกัดการเชื่อมต่อสูงสุด</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>บางครั้งผู้อ่านที่เป็นโปรแกรมเมอร์ได้เขียนโค้ดอย่างรอบคอบ มีการตรวจสอบแล้วว่าใช้สูงสุดแค่ 1 connection ต่อ 1 ผู้ใช้เท่านั้น และผู้ใช้งานบนเว็บจริงๆที่ online บน production site ก็ไม่ได้มากมายเกินร้อย แต่ทำไมถึงขึ้นปัญหาเกี่ยวกับการเชื่อมต่อถึงขีดจำกัดได้.<br />
ปัญหานี้ผู้เขียนเองก็เพิ่งจะมีโอกาสได้พบเป็นครั้งแรกจากการใช้ shared hosting. ที่ต้องบอกว่าพบเป็นครั้งแรกเพราะโดยทั่วไปที่พบมักเป็นปัญหาอื่น เช่น ผู้ใช้อื่นที่ใช้ host ร่วมกันเขียนโค้ดกินทรัพยากรเช่น RAM จน server ล่ม แม้แต่ HTML ก็เรียกดูไม่ได้, หรือบางกรณีก็แย่งกันใช้งาน DB server จน down หรือ table พังไปทั้งหมด ทำให้เรียกได้แต่ HTML หรือ PHP ที่ไม่ใช้ DB เป็นต้น.</p>
<p>ทางผู้เขียนจะขออธิบายและแสดงผลให้ดูตามลำดับของปัญหาดังนี้</p>
<h3>Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1040] Too many connections</h3>
<p>ปัญหานี้เกิดจากมีการเรียกใช้งานหรือเปิดการเชื่อมต่อไปยังเซิร์ฟเวอร์ฐานข้อมูล โดยรวมทั้งเซิร์ฟเวอร์แล้วจำนวนมากเกินกว่าจำนวนสูงสุดของ max_connections ที่ทาง web hosting กำหนดไว้ให้. ทดสอบได้บนเครื่องของเราเอง เริ่มจากการตรวจสอบโดยใช้คำสั่งนี้บน phpMyAdmin หรือโปรแกรมใดๆที่รันคำสั่งบน MySQL, MariaDB ได้.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-sql">SHOW VARIABLES WHERE `variable_name` = 'max_connections'</code></pre>
<p>เมื่อทดลองแล้ว จะได้ผลลัพธ์คือตัวเลขของจำนวนการเชื่อมต่อสูงสุดที่ทาง host นั้นๆอนุญาตให้โดยรวมทั้ง server. จากการทดลองบน localhost ของตัวเองคือ 151. หมายความว่า MariaDB ที่ติดตั้งบนเครื่องของผู้เขียนจะอนุญาตให้เชื่อมต่อโดยรวมได้สูงสุด 151 และถ้าหากตัวเลขนี้เป็นผลจากการรันบน web hosting จริงๆ ก็หมายความว่า web hosting นั้นๆอนุญาตให้โดยรวมต่อทั้ง server เป็นจำนวนเท่านั้นไม่ว่าผู้ใช้จะมีกี่รายก็ตาม.</p>
<p>ต่อมาให้เราใช้โค้ด PHP ต่อไปนี้ทดลองรันแล้วดูผลลัพธ์ โดยก่อนทดลอง จำเป็นต้อง<a href="https://rundiz.com/articles/%e0%b8%9b%e0%b8%a3%e0%b8%b1%e0%b8%9a%e0%b9%81%e0%b8%95%e0%b9%88%e0%b8%87-php-ini-%e0%b8%ad%e0%b8%a2%e0%b9%88%e0%b8%b2%e0%b8%87%e0%b9%84%e0%b8%a3%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%ab%e0%b8%a1">เปิดแสดง error ให้หมด</a>เสียก่อน.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-php">&lt;?php
$dsn = 'mysql:dbname=test;host=localhost;charset=utf8mb4';
$user = 'user';
$password = 'pass';
$options = [
    PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_STRINGIFY_FETCHES =&gt; true,
    PDO::ATTR_DEFAULT_FETCH_MODE =&gt; PDO::FETCH_OBJ,
];

$dbh = new PDO($dsn, $user, $password, $options);
for ($i = 2; $i &lt;= 151; ++$i) {
    ${'dbh' . $i} = new PDO($dsn, $user, $password, $options);
}// endfor;


$sql = 'SHOW STATUS WHERE `variable_name` LIKE \'Threads%\'';
$Sth = $dbh-&gt;prepare($sql);
$Sth-&gt;execute();
$result = $Sth-&gt;fetchAll();
unset($sql, $Sth);
var_dump($result);
unset($result);</code></pre>
<p>สิ่งที่ผู้อ่านต้องทำจากโค้ดด้านบนคือ แก้ไขค่าต่างๆ เช่น dbname, host, user, password ให้ตรงกับที่ใช้งาน จากนั้นแก้ไขตัวเลข 151 ให้เท่ากันกับตัวเลขที่ได้รับจากค่าจำนวนการเชื่อมต่อสูงสุด ของ MySQL, MariaDB ด้านบน. ทำการทดลองรันครั้งแรกควรจะได้ผลปกติไม่มี error ใดๆ และให้แก้ไขจากตัวเลข 151 ไปเป็นตัวเลขที่มากกว่าจำนวนการเชื่อมต่อสูงสุดของคุณ เช่น 152 สำหรับเครื่องของผม แล้วทดลองรันอีกครั้ง โดยคราวนี้จะเกิด error ขึ้นคือ <strong>Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1040] Too many connections</strong>.</p>
<p><a href="https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-too-many-connections-error.jpg"><img loading="lazy" decoding="async" width="941" height="217" class="aligncenter size-full wp-image-998 img-fluid" src="https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-too-many-connections-error.jpg" alt="Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1040] Too many connections" srcset="https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-too-many-connections-error.jpg 941w, https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-too-many-connections-error-300x69.jpg 300w, https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-too-many-connections-error-768x177.jpg 768w" sizes="auto, (max-width: 941px) 100vw, 941px" /></a></p>
<h3>Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1203] User user already has more than 'max_user_connections' active connections</h3>
<p>ปัญหานี้เกิดจากมีการเรียกใช้งานหรือเปิดการเชื่อมต่อไปยังเซิร์ฟเวอร์ฐานข้อมูล จำนวนต่อ account บน host เกินจำนวนสูงสุดที่ทาง web hosting ได้กำหนดไว้ให้. ทดสอบได้บนเครื่องของเราเอง โดยการ<a href="https://mariadb.com/kb/en/configuring-mariadb-with-option-files/" target="_blank" rel="noopener">แก้ไข my.cnf หรือ my.ini</a> ให้มีค่า <code>max_user_connections</code> ต่ำๆ เช่น 10 เป็นต้น. จากนั้นใช้โค้ดเดียวกันกับด้านบนทดลองรันแล้วจะได้ผลออกมาคือ <strong>Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1203] User user already has more than 'max_user_connections' active connections</strong>.</p>
<p><a href="https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-max_user_connections-error.jpg"><img loading="lazy" decoding="async" width="939" height="246" class="aligncenter size-full wp-image-997 img-fluid" src="https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-max_user_connections-error.jpg" alt="Fatal error: Uncaught PDOException: SQLSTATE[HY000] [1203] User user already has more than 'max_user_connections' active connections" srcset="https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-max_user_connections-error.jpg 939w, https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-max_user_connections-error-300x79.jpg 300w, https://rundiz.com/wp-content/uploads/2024/12/mysql-mariadb-max_user_connections-error-768x201.jpg 768w" sizes="auto, (max-width: 939px) 100vw, 939px" /></a></p>
<p>กลับมาที่กรณีที่มีปัญหากับ web hosting นั้นๆ <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f424.png" alt="🐤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> จากการทดลองด้วยตัวเองเหล่านี้ ทำให้พบว่าฝ่าย support นั้นไม่มีความรู้ความเข้าใจ จึงได้ยกคำตอบที่ไม่เกี่ยวกันมาตอบปัญหา เช่นไปยกเอาค่า Threads_created มาบอกว่าเป็นจำนวนการเชื่อมต่อสูงสุดของ server ซึ่งไม่ตรงตามที่อธิบายโดยทั้ง <a href="https://dev.mysql.com/doc/refman/8.4/en/server-status-variables.html#statvar_Threads_created" target="_blank" rel="noopener">MySQL</a> และ <a href="https://mariadb.com/kb/en/server-status-variables/#threads_created" target="_blank" rel="noopener">MariaDB</a> เอง อีกทั้งยังยกเรื่อง performance ปริมาณการโหลด ซึ่งไม่เกี่ยวกันมาอีก เพราะปัญหาจำนวนการเชื่อมต่อที่มากเกินอนุญาตนั้น อาจมีผลกับการที่ server ทำงานหนักมากและจำนวนอนุญาตไว้มาก หรือจำนวนการเชื่อมต่อ อนุญาตไว้เพียงเล็กน้อยและ server ยังไม่ทันได้ทำงานหนักเลยก็เป็นไปได้ทั้งสองแบบ.</p>
<h3>สรุป</h3>
<p>ดังนั้นสิ่งที่ผู้อ่านควรตระหนักเมื่อเลือกใช้ shared hosting (อาจจะกรณีเช่า host ใหม่หรือย้ายไป host ใหม่) ในเรื่องที่เกี่ยวกับประเด็นนี้ก็คือ จำนวน <code>Threads_connected</code>. ตรวจสอบโดยใช้คำสั่ง MySQL ต่อไปนี้.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-sql">SHOW STATUS WHERE `variable_name` = 'Threads_connected'</code></pre>
<p>จำนวนของค่านี้ ให้เราทดลองเรียกใช้เมื่อยังไม่ได้เปิดเว็บให้ online เต็มที่. กล่าวคือติดตั้งโดเมนทดลองแล้วเรียกใช้เพียงคนเดียว จำนวน <code>Threads_connected</code> จะสะท้อนทั้งโฮสท์ว่ามีคนใช้งานร่วมกับเราอยู่เท่าไหร่.</p>
<p>ต่อมาคือจำนวน <code>max_connections</code>. ให้ตรวจสอบค่านี้โดยใช้คำสั่ง SQL ด้านบนในหัวข้อ Too many connections. จำนวนค่านี้จะหมายถึงจำนวนสูงสุดต่อทั้ง server ที่ทางผู้ให้บริการกำหนดไว้ ซึ่งถ้าจำนวน <code>Threads_connected</code> ปริ่มๆจะเต็มอยู่แล้ว ก็พิจารณาขอย้าย server โดยให้เหตุผลตามนี้ไปว่ามันใกล้จะเต็มหรืออาจขอยกเลิกบริการและขอคืนเงินตามเงื่อนไข.</p>
<p>สุดท้ายคือจำนวน <code>max_user_connections</code>. ให้ตรวจสอบค่านี้โดยใช้คำสั่ง SQL ต่อไปนี้.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-sql">SHOW VARIABLES WHERE `variable_name` = 'max_user_connections'</code></pre>
<p>คำสั่งนี้จะเป็นจำนวนการเชื่อมต่อฐานข้อมูลสูงสุดต่อ 1 ผู้ใช้คือ account ของคุณบน shared hosting. การตรวจสอบจะทำได้ยากสักหน่อยเพราะจำนวน <code>Threads_connected</code> จะเป็นจำนวนรวมต่อทั้ง server ซึ่งคุณอาจใช้โค้ดด้านบนตรวจสอบก็ได้ โดยกำหนดจำนวน loop ของ for ให้พอดีหรือเกิน <code>max_user_connections</code> เพื่อจะได้ทราบว่าสูงสุดที่เกินนั้นตรงความจริงหรือไม่.</p>
<p>The post <a href="https://rundiz.com/blog/articles/mysql-mariadb-%e0%b8%81%e0%b8%b1%e0%b8%9a%e0%b8%82%e0%b8%b5%e0%b8%94%e0%b8%88%e0%b8%b3%e0%b8%81%e0%b8%b1%e0%b8%94%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b9%80%e0%b8%8a%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%a1/">MySQL (MariaDB) กับขีดจำกัดการเชื่อมต่อสูงสุด</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/mysql-mariadb-%e0%b8%81%e0%b8%b1%e0%b8%9a%e0%b8%82%e0%b8%b5%e0%b8%94%e0%b8%88%e0%b8%b3%e0%b8%81%e0%b8%b1%e0%b8%94%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b9%80%e0%b8%8a%e0%b8%b7%e0%b9%88%e0%b8%ad%e0%b8%a1/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>วิธีขยาย (micro) SD card บน Raspberry Pi โดยใช้ GUI</title>
		<link>https://rundiz.com/blog/articles/%e0%b8%a7%e0%b8%b4%e0%b8%98%e0%b8%b5%e0%b8%82%e0%b8%a2%e0%b8%b2%e0%b8%a2-micro-sd-card-%e0%b8%9a%e0%b8%99-raspberry-pi-%e0%b9%82%e0%b8%94%e0%b8%a2%e0%b9%83%e0%b8%8a%e0%b9%89-gui/</link>
					<comments>https://rundiz.com/blog/articles/%e0%b8%a7%e0%b8%b4%e0%b8%98%e0%b8%b5%e0%b8%82%e0%b8%a2%e0%b8%b2%e0%b8%a2-micro-sd-card-%e0%b8%9a%e0%b8%99-raspberry-pi-%e0%b9%82%e0%b8%94%e0%b8%a2%e0%b9%83%e0%b8%8a%e0%b9%89-gui/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Tue, 16 Jul 2024 09:48:46 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[debian]]></category>
		<category><![CDATA[raspberry pi]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=967</guid>

					<description><![CDATA[<p>สำหรับผู้ใช้ Raspberry Pi นั้นโดยปกติเขามักจะแนะนำให้ลง &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%a7%e0%b8%b4%e0%b8%98%e0%b8%b5%e0%b8%82%e0%b8%a2%e0%b8%b2%e0%b8%a2-micro-sd-card-%e0%b8%9a%e0%b8%99-raspberry-pi-%e0%b9%82%e0%b8%94%e0%b8%a2%e0%b9%83%e0%b8%8a%e0%b9%89-gui/">วิธีขยาย (micro) SD card บน Raspberry Pi โดยใช้ GUI</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>สำหรับผู้ใช้ Raspberry Pi นั้นโดยปกติเขามักจะแนะนำให้ลง OS ที่อยู่บนฐานของระบบ Debian ดังนั้นคำสั่งในบทความนี้จะอยู่บนพื้นฐานของ Debian.</p>
<p>การขยาย SD card นี้ อธิบายขยายความก็คือการ copy, clone, upgrade SD card หรือ micro SD card นั่นเอง. และขั้นตอนต่อไปนี้เป็นการทำผ่าน GUI หรือหน้า desktop ซึ่งเข้าใจง่ายและใช้งานง่าย.</p>
<p>ก่อนเริ่มต้น สมมุติว่า Raspberry Pi OS ที่คุณใช้งานนั้นเป็น micro SD ที่เสียบในสล็อตของตัว Raspberry Pi (จากนี้จะขอเรียกสั้นๆว่า <strong>R Pi</strong>). และ micro SD ตัวใหม่เสียบผ่าน card reader ทางช่อง USB.<br />
เมื่อเสียบ micro SD card ตัวใหม่เข้าไปใน card reader แล้ว. จากหน้า desktop ของคุณใน R Pi (Debian). ให้คลิกเมนูเริ่มต้น หรือที่มีรูป Raspberry สีแดงๆ แล้วไปที่ &gt; Accessories &gt; SD Card Copier.</p>
<div class="alert alert-secondary">
<p>ถ้าหากโปรแกรมนี้ไม่มี ให้เปิด terminal ขึ้นมา แล้วพิมพ์คำสั่งต่อไปนี้.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-bash">sudo apt update
sudo apt install piclone</code></pre>
<p>โดยพิมพ์คำสั่งแล้ว enter ทีละบรรทัด แล้วกดยืนยันหรือ <kbd>Y</kbd> (ถ้ามี). จากนั้นรอจนติดตั้งเสร็จแล้วจึงเปิด SD Card Copier ขึ้นมา.</p>
</div>
<p>สำหรับภาพตัวอย่างจะเป็นการทำการอัปเกรด, หรือขยายขนาด micro SD card ที่ใช้รัน R Pi OS อยู่ โดยขยายจาก 64 GB ไปเป็น 256 GB.</p>
<p><a href="https://rundiz.com/wp-content/uploads/2024/07/01-sd-copy.png"><img loading="lazy" decoding="async" width="1024" height="576" class="img img-fluid aligncenter size-large wp-image-969" src="https://rundiz.com/wp-content/uploads/2024/07/01-sd-copy-1024x576.png" alt="" srcset="https://rundiz.com/wp-content/uploads/2024/07/01-sd-copy-1024x576.png 1024w, https://rundiz.com/wp-content/uploads/2024/07/01-sd-copy-300x169.png 300w, https://rundiz.com/wp-content/uploads/2024/07/01-sd-copy-768x432.png 768w, https://rundiz.com/wp-content/uploads/2024/07/01-sd-copy.png 1253w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><br />
จากภาพ, ตัวเลือกตรงบรรทัด Copy From Device: ให้เลือกจากการ์ดที่กำลังใช้งาน R Pi OS อยู่ ซึ่งอาจเป็นชื่ออื่นก็ได้ แต่ถ้าเสียบผ่านสล็อตบนตัวอุปกรณ์ R Pi จะไม่ใช่ USB. ขั้นนี้ต้องเลือกและระมัดระวังตรวจให้แน่ใจด้วยตัวเอง.<br />
จากนั้นตัวเลือกตรงบรรทัด Copy To Device: ให้เลือกการ์ดตัวใหม่ที่จะอัปเกรดหรือขยายพื้นที่เพิ่มมากขึ้น. ถ้าหากเสียบผ่าน card reader ทางช่อง USB ให้เลือกตัวเลือกที่เป็น USB โดยตรวจดูชื่อเทียบเคียงกับคำสั่ง <code>df -h</code> ที่ทำงานตามภาพตัวอย่างในหน้าต่างด้านขวา.</p>
<p>กดปุ่ม Start แล้วยืนยันเพื่อเริ่มทำงานคัดลอก micro SD card จากตัวเก่าไปยังตัวใหม่.</p>
<p><a href="https://rundiz.com/wp-content/uploads/2024/07/02-prepare-partitioin.png"><img loading="lazy" decoding="async" width="1024" height="576" class="img img-fluid aligncenter size-large wp-image-970" src="https://rundiz.com/wp-content/uploads/2024/07/02-prepare-partitioin-1024x576.png" alt="" srcset="https://rundiz.com/wp-content/uploads/2024/07/02-prepare-partitioin-1024x576.png 1024w, https://rundiz.com/wp-content/uploads/2024/07/02-prepare-partitioin-300x169.png 300w, https://rundiz.com/wp-content/uploads/2024/07/02-prepare-partitioin-768x432.png 768w, https://rundiz.com/wp-content/uploads/2024/07/02-prepare-partitioin.png 1253w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><br />
จากภาพด้านบน เป็นขั้นตอนที่กำลังคัดลอกไฟล์ไปยัง micro SD card ตัวใหม่โดยโปรแกรมจะทำการสร้างพาร์ติชั่น (partition) ให้ด้วย. ในขั้นตอนนี้เมื่อคัดลอกสำเร็จแล้ว ให้สั่ง shutdown เพื่อปิดเครื่องอย่างสมบูรณ์ จากนั้นถอด micro SD card ทั้งสองออก แล้วเสียบการ์ดตัวใหม่ที่จะทำการอัปเกรดหรือขยายพื้นที่เข้าไปยังสล็อตบนเครื่อง R Pi. ทำการดึงปลั๊กและเสียบปลั๊กไฟเข้าไปใหม่เพื่อเปิดเครื่อง (ขั้นตอนสำหรับเครื่องปกติที่ไม่มีอุปกรณ์พิเศษ).</p>
<p><a href="https://rundiz.com/wp-content/uploads/2024/07/03-completed.png"><img loading="lazy" decoding="async" width="1024" height="576" class="img img-fluid aligncenter size-large wp-image-971" src="https://rundiz.com/wp-content/uploads/2024/07/03-completed-1024x576.png" alt="" srcset="https://rundiz.com/wp-content/uploads/2024/07/03-completed-1024x576.png 1024w, https://rundiz.com/wp-content/uploads/2024/07/03-completed-300x169.png 300w, https://rundiz.com/wp-content/uploads/2024/07/03-completed-768x432.png 768w, https://rundiz.com/wp-content/uploads/2024/07/03-completed.png 1253w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></a><br />
จากภาพด้านบน เมื่อเปิดเครื่องแล้วให้รอสักครู่ ซึ่งอาจจะนานกว่าปกติ เพราะตัวระบบจะทำการตรวจสอบ micro SD card ตัวใหม่แล้วขยายพื้นที่บน partition ที่อาจจะเหลือให้เต็ม เพื่อการใช้งานเต็มประสิทธิภาพ.  เมื่อเข้าสู่หน้า desktop ได้แล้ว ให้เปิด terminal แล้วพิมพ์คำสั่ง <code>df -h</code> ตัวโปรแกรมควรจะแสดงพื้นที่ทั้งหมดบน micro SD card ตัวใหม่อย่างถูกต้อง เป็นอันเสร็จสิ้นขั้นตอนทั้งหมด.</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%a7%e0%b8%b4%e0%b8%98%e0%b8%b5%e0%b8%82%e0%b8%a2%e0%b8%b2%e0%b8%a2-micro-sd-card-%e0%b8%9a%e0%b8%99-raspberry-pi-%e0%b9%82%e0%b8%94%e0%b8%a2%e0%b9%83%e0%b8%8a%e0%b9%89-gui/">วิธีขยาย (micro) SD card บน Raspberry Pi โดยใช้ GUI</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/%e0%b8%a7%e0%b8%b4%e0%b8%98%e0%b8%b5%e0%b8%82%e0%b8%a2%e0%b8%b2%e0%b8%a2-micro-sd-card-%e0%b8%9a%e0%b8%99-raspberry-pi-%e0%b9%82%e0%b8%94%e0%b8%a2%e0%b9%83%e0%b8%8a%e0%b9%89-gui/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>แพลตฟอร์มที่รองรับการขายแบบ subscription ไม่มีรายเดือน (2024)</title>
		<link>https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%9e%e0%b8%a5%e0%b8%95%e0%b8%9f%e0%b8%ad%e0%b8%a3%e0%b9%8c%e0%b8%a1%e0%b8%97%e0%b8%b5%e0%b9%88%e0%b8%a3%e0%b8%ad%e0%b8%87%e0%b8%a3%e0%b8%b1%e0%b8%9a%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%82/</link>
					<comments>https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%9e%e0%b8%a5%e0%b8%95%e0%b8%9f%e0%b8%ad%e0%b8%a3%e0%b9%8c%e0%b8%a1%e0%b8%97%e0%b8%b5%e0%b9%88%e0%b8%a3%e0%b8%ad%e0%b8%87%e0%b8%a3%e0%b8%b1%e0%b8%9a%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%82/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Tue, 02 Jan 2024 07:03:44 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[ecommerce]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=931</guid>

					<description><![CDATA[<p>หากคุณกำลังมองหาช่องทางที่จะขายของแบบ subscription หรือ &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%9e%e0%b8%a5%e0%b8%95%e0%b8%9f%e0%b8%ad%e0%b8%a3%e0%b9%8c%e0%b8%a1%e0%b8%97%e0%b8%b5%e0%b9%88%e0%b8%a3%e0%b8%ad%e0%b8%87%e0%b8%a3%e0%b8%b1%e0%b8%9a%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%82/">แพลตฟอร์มที่รองรับการขายแบบ subscription ไม่มีรายเดือน (2024)</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>หากคุณกำลังมองหาช่องทางที่จะขายของแบบ subscription หรือก็คือแบบที่ลูกค้าจะต้องจ่ายเป็นระยะ เช่น รายวัน, สัปดาห์, เดือน, ปี จึงจะใช้งานได้ต่อเนื่อง. โพสต์นี้จะแนะนำช่องทางการขายสินค้าแบบ subscription บนแพลตฟอร์มต่างๆที่รองรับ พร้อมทั้งเปรียบเทียบความสามารถและข้อจำกัดต่างๆ.</p>
<p>เนื่องจากผู้ให้บริการขายสินค้าแบบ subscription นั้นมีจำนวนมาก แต่ส่วนใหญ่จะคิดค่าบริการรายเดือน และถ้าหากคุณขายไม่ได้ก็เท่ากับคุณจะต้องเสียเงินไปฟรีๆทุกเดือน. ดังนั้นโพสต์นี้จึงเน้นเฉพาะแพลตฟอร์มที่ไม่คิดค่าบริการรายเดือน แต่จะเก็บเป็น % ต่อยอดขายแทน ซึ่งข้อดีคือถ้าขายได้น้อยหรือขายยาก ก็จะไม่ต้องเสียค่าใช้จ่าย เหมาะสำหรับผู้เริ่มต้นที่ยังมีฐานลูกค้าไม่มากนัก.</p>
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th></th>
<th><a href="https://www.lemonsqueezy.com/" target="_blank" rel="noopener">Lemon squeezy</a></th>
<th><a href="https://www.paddle.com" target="_blank" rel="noopener">Paddle</a></th>
<th><a href="https://payhip.com" target="_blank" rel="noopener">Payhip</a></th>
</tr>
</thead>
<tbody>
<tr>
<td>ค่าบริการ</td>
<td>5% + 50¢</td>
<td>5% + 50¢</td>
<td>5%</td>
</tr>
<tr>
<td>การขาย subscription</td>
<td>รายสัปดาห์, รายเดือน, รายปี, กำหนดเอง</td>
<td>รายเดือน, รายปี, กำหนดเอง</td>
<td>รายเดือน, รายปี, กำหนดเอง</td>
</tr>
<tr>
<td>การจ่ายเงินผู้ขาย</td>
<td>PayPal, โอนเงินผ่านธนาคาร (รองรับไทย)</td>
<td>PayPal, Payoneer, โอนเงินผ่านธนาคาร</td>
<td>PayPal, Stripe</td>
</tr>
<tr>
<td>การจ่ายเงินผู้ขายขั้นต่ำ</td>
<td>$50</td>
<td>$100</td>
<td>ไม่มี</td>
</tr>
<tr>
<td>รองรับการชำระเงิน</td>
<td>บัตรเครดิต-เดบิต, PayPal, Apple pay, Google pay</td>
<td>บัตรเครดิต-เดบิต, PayPal, Apple pay, Google pay</td>
<td>PayPal, บัตรเครดิต-เดบิตผ่าน PayPal</td>
</tr>
</tbody>
</table>
</div>
<p>จากตารางจะพบว่า Lemon squeezy และ Paddle รองรับช่องทางการชำระเงินจากลูกค้าที่หลากหลายมากกว่า แต่ Lemon squeezy จะจ่ายเงินเมื่อมียอดขั้นต่ำน้อยกว่า คือต้องมียอดเงินในระบบของเขาตั้งแต่ 50 ดอลล่าร์ขึ้นไป. ในขณะที่ Payhip ช่องทางการชำระเงินมีน้อยมากแต่ก็เป็นที่ใช้งานอย่างกว้างขวาง (PayPal) แถมยังรองรับการชำระด้วยบัตรผ่าน PayPal. ทั้งนี้ในส่วนการจ่ายเงินผู้ขายของ Payhip เขาจะโอนเงินตรงเข้า PayPal หรือ Stripe ทันทีเมื่อผู้ซื้อชำระเงิน.</p>
<p>แพลตฟอร์มทั้งหลายเหล่านี้ต่างมีข้อดีตรงกันคือ ผู้ขายไม่จำเป็นต้องมีเว็บไซต์ และถ้ามีก็สามารถนำไปใช้ได้เช่นกันเพราะมี API รองรับ.</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%9e%e0%b8%a5%e0%b8%95%e0%b8%9f%e0%b8%ad%e0%b8%a3%e0%b9%8c%e0%b8%a1%e0%b8%97%e0%b8%b5%e0%b9%88%e0%b8%a3%e0%b8%ad%e0%b8%87%e0%b8%a3%e0%b8%b1%e0%b8%9a%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%82/">แพลตฟอร์มที่รองรับการขายแบบ subscription ไม่มีรายเดือน (2024)</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/%e0%b9%81%e0%b8%9e%e0%b8%a5%e0%b8%95%e0%b8%9f%e0%b8%ad%e0%b8%a3%e0%b9%8c%e0%b8%a1%e0%b8%97%e0%b8%b5%e0%b9%88%e0%b8%a3%e0%b8%ad%e0%b8%87%e0%b8%a3%e0%b8%b1%e0%b8%9a%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%82/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>การสร้าง URL แบบกำหนดเอง (rewrite rules)</title>
		<link>https://rundiz.com/blog/articles/%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-url-%e0%b9%81%e0%b8%9a%e0%b8%9a%e0%b8%81%e0%b8%b3%e0%b8%ab%e0%b8%99%e0%b8%94%e0%b9%80%e0%b8%ad%e0%b8%87-rewrite-rules/</link>
					<comments>https://rundiz.com/blog/articles/%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-url-%e0%b9%81%e0%b8%9a%e0%b8%9a%e0%b8%81%e0%b8%b3%e0%b8%ab%e0%b8%99%e0%b8%94%e0%b9%80%e0%b8%ad%e0%b8%87-rewrite-rules/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Fri, 01 Dec 2023 09:22:49 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=926</guid>

					<description><![CDATA[<p>การสร้าง URI/URL แบบกำหนดเอง โดยกำหนดจากจุดเริ่มต้นของต &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-url-%e0%b9%81%e0%b8%9a%e0%b8%9a%e0%b8%81%e0%b8%b3%e0%b8%ab%e0%b8%99%e0%b8%94%e0%b9%80%e0%b8%ad%e0%b8%87-rewrite-rules/">การสร้าง URL แบบกำหนดเอง (rewrite rules)</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>การสร้าง URI/URL แบบกำหนดเอง โดยกำหนดจากจุดเริ่มต้นของตำแหน่งติดตั้ง WordPress สามารถทำได้อย่างน้อย 2 วิธีจากที่ได้ทดลองมาแล้ว. ตัวอย่างของ URL แบบกำหนดเอง เช่น https://domain/custompage และ https://domain/custompage/my โดยในเงื่อนไขคือจะต้องรองรับหลายภาษาจาก Polylang ได้ด้วย.</p>
<h3>แบบไม่ใช้หน้า (Pages) ของ WordPress</h3>
<p>กรณีแรก จะใช้แบบ URL กำหนดเอง (rewrite rules) ล้วนๆ โดยไม่พึ่งพาระบบหน้า (Pages) ของ WordPress. ซึ่งข้อดีคือเราสามารถทำให้ URL ใช้สลัก(slug)เดียวกันได้ ดังตัวอย่างนี้.<br />
https://domain/custompage (สำหรับภาษาเริ่มต้น ที่ไม่ได้แสดงภาษาใน URL),<br />
https://domain/custompage/my,<br />
https://domain/en/custompage,<br />
https://domain/en/custompage/my</p>
<h4>ซอร์สโค้ด</h4>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-php">&lt;?php
class Routes
{


    /**
     * @var string Main query name for the page after /custompage/ URL.
     */
    public const MAIN_QUERY_NAME = 'custompage';


    /**
     * Constructor.
     */
    public function __construct()
    {
        add_action('init', [$this, 'addRewriteRules']);
        add_action('template_redirect', [$this, 'runPages']);
    }// __construct


    /**
     * Add rewrite rules.
     */
    public function addRewriteRules()
    {
        add_rewrite_rule('^([a-zA-Z]{0,2}/?)custompage/?$', 'index.php?lang=$matches[1]&amp;' . static::MAIN_QUERY_NAME . '=index', 'top');
        add_rewrite_rule('^([a-zA-Z]{0,2}/?)custompage/my?$', 'index.php?lang=$matches[1]&amp;' . static::MAIN_QUERY_NAME . '=mycustompage', 'top');

        add_rewrite_tag('%' . static::MAIN_QUERY_NAME . '%', '([^&amp;]+)');
    }// addRewriteRules


    /**
     * Run the pages based on query(s).
     */
    public function runPages()
    {
        $mainQueryVal = get_query_var(static::MAIN_QUERY_NAME, null);

        if (!is_null($mainQueryVal)) {
            switch ($mainQueryVal) {
                default:
                    var_dump($mainQueryVal);
                    echo 'hello world';
            }// endswitch;

            exit();// required! to prevent WordPress continue executing. and don't use `wp_die()`.
        }// endif;

        unset($mainQueryVal);
    }// runPages


}</code></pre>
<p>ข้อเสียสำหรับตัวอย่างด้านบน คือ เมื่อตรวจสอบด้วยคำสั่งบนเท็มเพลท เช่น <code class="rd-syntax-highlighter language-php">is_home()</code> ภายในเมธ็อด <code class="rd-syntax-highlighter language-php">runPages()</code> จะได้ค่าเป็น <code class="rd-syntax-highlighter language-php">true</code> เสมอ. วิธีแก้ไขคือ เพิ่ม parameter <code class="rd-syntax-highlighter language-php">'pagename='</code> เข้าไปใน <code class="rd-syntax-highlighter language-php">add_rewrite_rule()</code> เช่น:</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="rd-syntax-highlighter language-php">add_rewrite_rule('^([a-zA-Z]{0,2}/?)custompage/?$', 'index.php?lang=$matches[1]&amp;' . static::MAIN_QUERY_NAME . '=index&amp;pagename=mycustompage', 'top')</code></pre>
<p>แต่ก็จะแลกมากับสถานะ HTTP เป็น 404 ในกรณีที่คุณใส่ชื่อหน้าที่ไม่มีอยู่บนระบบหน้า (Pages) ของ WordPress ซึ่งก็จะต้องมาแก้ไขโดยการกำหนด HTTP status และหัวข้อของหน้านั้นใหม่ และกำหนด <code class="rd-syntax-highlighter language-php">$wp_query-&gt;is_404 = false;</code> ภายใน hook <a href="https://developer.wordpress.org/reference/hooks/template_redirect/" target="_blank" rel="noopener">template_redirect</a>. นอกจากนี้ก็จะต้องแก้ไขแท็ก <code class="rd-syntax-highlighter language-markup">title</code> ให้เหมาะผ่าน filter <a href="https://developer.wordpress.org/reference/hooks/document_title_parts/" target="_blank" rel="noopener">document_title_parts</a>.</p>
<p>คุณอาจดูอ้างอิงเพื่อใส่ <a href="https://codex.wordpress.org/WordPress_Query_Vars" target="_blank" rel="noopener">query vars</a> อื่นๆได้อีก.</p>
<h3>แบบใช้หน้า (Pages) ของ WordPress</h3>
<p>แบบที่สองนี้ผู้อ่านจะต้องไปสร้างหน้าขึ้นมา เช่น หน้าภาษาไทยชื่อสลัก "หน้าที่กำหนดเอง" และแปลเป็นอังกฤษผ่าน Polylang ชื่อสลัก "custompage". จากนั้นนำ ID ของทั้ง 2 หน้านี้มาใส่ใน <code class="rd-syntax-highlighter language-php">array()</code> เช่น <code class="rd-syntax-highlighter language-php">array(165, 166)</code> หรือใช้ฟังก์ชั่นของ Polylang เช่น <code class="rd-syntax-highlighter language-php">pll_get_post_translations(165);</code> เพื่อดึงค่า post ID ทั้งหมดจากทุกภาษาที่เป็นหน้าแปลของ ID ของหน้าหลักที่เลือกไว้.</p>
<p>วิธีนี้ผู้อ่านจำเป็นจะต้องดึงเอา post ID จากหน้าของภาษาหลัก และ ID ของ<span style="text-decoration: underline;">ทุกๆ</span>ภาษาที่สร้างหน้าแปลขึ้นมาแล้ว นำมาเพิ่มลงใน rewrite rule ในคราวเดียวกัน. มิฉะนั้น WordPress จะไม่สามารถอ่านค่า rewrite rule ของ ID ของหน้านั้นในภาษาอื่นๆได้ เนื่องจากมันทำงานไปแล้วและติดอยู่ใน cache ซึ่งจะต้องทำการ flush ใหม่โดยไม่จำเป็นและสิ้นเปลืองทรัพยากร server. การเพิ่ม rewrite rule ทุก ID (จากหน้าที่แปลทุกภาษา)ในคราวเดียวจะช่วยแก้ปัญหานี้.</p>
<p>การใช้รูปแบบหน้าของ WordPress นี้นั้น ผู้อ่านจะไม่สามารถใช้สลักเดียวกันได้ ยกเว้นแต่ได้ซื้อปลั๊กอิน Polylang Pro เอาไว้. ดังนั้น URL จะได้เป็นลักษณะคล้ายๆดังต่อไปนี้.<br />
https://domain/หน้าที่กำหนดเอง<br />
https://domain/หน้าที่กำหนดเอง/my<br />
https://domain/en/custompage<br />
https://domain/en/custompage/my</p>
<h4>ซอร์สโค้ด</h4>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-php">class Routes
{


    /**
     * @var string Main query name for the page after /custompage/ URL.
     */
    public const MAIN_QUERY_NAME = 'custompage';


    /**
     * Constructor.
     */
    public function __construct()
    {
        add_action('init', [$this, 'addRewriteRules']);
        add_action('template_redirect', [$this, 'runPages']);
    }// __construct


    /**
     * Add rewrite rules.
     */
    public function addRewriteRules()
    {
        // IDs of a post and translated posts. 165 is main post (page).
        $postIds = pll_get_post_translations(165);
        $postIds = array_values($postIds);

        foreach ($postIds as $postId) {
            $Post = get_post($postId);
            if (!is_object($Post)) {
                continue;
            }

            add_rewrite_rule('^([a-zA-Z]{0,2}/?)' . rawurldecode($Post-&gt;post_name) . '/?$', 'index.php?lang=$matches[1]&amp;pagename=' . $Post-&gt;post_name . '&amp;' . static::MAIN_QUERY_NAME . '=index_usepage', 'top');
            add_rewrite_rule('^([a-zA-Z]{0,2}/?)' . rawurldecode($Post-&gt;post_name) . '/my/?$', 'index.php?lang=$matches[1]&amp;pagename=' . $Post-&gt;post_name . '&amp;' . static::MAIN_QUERY_NAME . '=mycustompage', 'top');
            unset($Post);
        }// endforeach;
        unset($postId, $postIds);

        add_rewrite_tag('%' . static::MAIN_QUERY_NAME . '%', '([^&amp;]+)');
    }// addRewriteRules


    /**
     * Run the pages based on query(s).
     */
    public function runPages()
    {
        $mainQueryVal = get_query_var(static::MAIN_QUERY_NAME, null);

        if (!is_null($mainQueryVal)) {
            switch ($mainQueryVal) {
                default:
                    var_dump($mainQueryVal);
                    echo 'hello world';
            }// endswitch;

            exit();// required! to prevent WordPress continue executing. and don't use `wp_die()`.
        }// endif;

        unset($mainQueryVal);
    }// runPages


}</code></pre>
<h3>ข้อสำคัญ</h3>
<p>เมื่อทำการเพิ่มหรือแก้ไขโค้ดใน <code class="rd-syntax-highlighter language-php">add_rewrite_rule()</code> แล้ว ให้ทำการกดบันทึกที่หน้าตั้งค่าลิ้งค์ถาวร (Permalink) ด้วยทุกครั้ง เพื่อให้ระบบทำการ flush ของเก่าออกไปแล้วนำของใหม่มาใช้งาน.</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-url-%e0%b9%81%e0%b8%9a%e0%b8%9a%e0%b8%81%e0%b8%b3%e0%b8%ab%e0%b8%99%e0%b8%94%e0%b9%80%e0%b8%ad%e0%b8%87-rewrite-rules/">การสร้าง URL แบบกำหนดเอง (rewrite rules)</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/%e0%b8%81%e0%b8%b2%e0%b8%a3%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-url-%e0%b9%81%e0%b8%9a%e0%b8%9a%e0%b8%81%e0%b8%b3%e0%b8%ab%e0%b8%99%e0%b8%94%e0%b9%80%e0%b8%ad%e0%b8%87-rewrite-rules/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>สร้าง self-signed certificate และทำให้เบราว์เซอร์เชื่อถือ</title>
		<link>https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-self-signed-certificate-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%97%e0%b8%b3%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%9a%e0%b8%a3%e0%b8%b2%e0%b8%a7%e0%b9%8c/</link>
					<comments>https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-self-signed-certificate-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%97%e0%b8%b3%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%9a%e0%b8%a3%e0%b8%b2%e0%b8%a7%e0%b9%8c/#respond</comments>
		
		<dc:creator><![CDATA[vee]]></dc:creator>
		<pubDate>Sun, 01 Oct 2023 10:52:14 +0000</pubDate>
				<category><![CDATA[บทความ]]></category>
		<category><![CDATA[ssl]]></category>
		<guid isPermaLink="false">https://rundiz.com/?p=911</guid>

					<description><![CDATA[<p>บทความนี้จะแสดงวิธีการสร้าง self-signed certificate เอา &#8230;</p>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-self-signed-certificate-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%97%e0%b8%b3%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%9a%e0%b8%a3%e0%b8%b2%e0%b8%a7%e0%b9%8c/">สร้าง self-signed certificate และทำให้เบราว์เซอร์เชื่อถือ</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>บทความนี้จะแสดงวิธีการสร้าง self-signed certificate เอาไว้ใช้เอง อย่างง่ายๆ และทำให้เบราว์เซอร์รวมถึงอุปกรณ์มือถืออย่างเช่น Android เชื่อถือได้อีกด้วย.</p>
<p>โดยปกติ certificate จะมีกระบวนการต่างๆกว่าจะนำเข้าสู่ระบบหรือเว็บเบราว์เซอร์และได้รับความเชื่อถือ (trust) อย่างเช่น SSL ของ Let's encrypt ซึ่งถ้าหากเราสร้างขึ้นมาเอง จะไม่สามารถผ่านกระบวนการเหล่านั้นได้. แต่กระนั้นการจะใช้ SSL ที่มีในท้องตลาดไม่ว่าจะฟรีหรือเสียเงิน บางกรณีเราไม่สามารถจะสร้างอย่างถูกต้องได้เพราะติดขัดในขั้นตอนต่างๆ เช่น Let's encrypt ต้องการจะเข้าถึง HTTP port 80 ได้ เพื่อออกใบ cert แต่เครื่องคอมพิวเตอร์ที่เราใช้ ไม่มี public IP หรือมีแต่ไม่สามารถเลือก port ได้ เป็นต้น. กรณีอย่างนี้ก็จะต้องหาทางออกโดยใช้ self-signed certificate.</p>
<h2>ใช้ OpenSSL</h2>
<p>ดาวน์โหลดและติดตั้ง OpenSSL ได้จาก <a href="https://www.openssl.org/source/" target="_blank" rel="noopener">https://www.openssl.org/source/</a> หรือสำหรับบน Windows เลือกดาวน์โหลดได้ที่ <a href="https://wiki.openssl.org/index.php/Binaries" target="_blank" rel="noopener">https://wiki.openssl.org/index.php/Binaries</a>. หากคุณใช้เว็บเซิร์ฟเวอร์ Apache บน Windows มันจะมาด้วยกันกับไฟล์ httpd.exe.</p>
<h3>เพิ่ม path ไปยัง openssl.exe ไว้ใน Windows environment.</h3>
<p>เมื่อติดตั้ง OpenSSL เสร็จแล้ว ให้เพิ่ม path ไปยัง <strong>openssl.exe</strong> ไว้ใน Windows environment <code>PATH</code> เพื่อจะเรียกคำสั่ง <code>openssl</code> ได้โดยง่าย.</p>
<ol>
<li>กดเมนู Start</li>
<li>พิมพ์ systempropertiesadvanced.exe</li>
<li>คลิก Environment Variables</li>
<li>ที่กรอบ System Variables ดับเบิลคลิกที่ PATH</li>
<li>เพิ่ม path ไปยัง openssl.exe</li>
<li>กด OK ทั้งหมด</li>
</ol>
<h3>สร้าง Root cert.</h3>
<p>ขั้นตอนนี้ ผู้อ่านอาจจะสร้างไฟล์ต่างๆไว้ในโฟลเดอร์ของ Apache/conf ก็ได้ เพื่อความสะดวกในการกำหนด config ไปด้วย. ให้เข้าไปที่โฟลเดอร์ดังกล่าวแล้วพิมพ์คำสั่งต่อไปนี้.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-bash">openssl req -x509 -sha256 -days 3650 -nodes -newkey rsa:2048 -subj="/CN=localhost.localhost/C=TH/L=Bangkok" -keyout rootCA.key -out rootCA.crt</code></pre>
<p>จากคำสั่งดังกล่าว โปรแกรมจะสร้างไฟล์ rootCA.key และ rootCA.crt มาให้ 2 ไฟล์ ซึ่งทั้ง 2 ไฟล์นี้จะมีประโยชน์อย่างมากเมื่อนำเข้าไปในเว็บเบราเซอร์ให้เชื่อถือ.</p>
<h3>สร้าง Certificate</h3>
<p>ขั้นตอนนี้จะอยู่ในโฟลเดอร์เดียวกันกับขั้นตอนสร้าง Root certificate. ให้สร้างไฟล์ config ชื่อ server.cnf แล้วระบุข้อมูลต่อไปนี้ โดยแก้ไขส่วนต่างๆให้ตรงตามความจริงของผู้อ่านเอง.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-ini">[req]
default_bits = 2048
prompt = no
default_md = sha256
x509_extensions = v3_req # The extensions to add to the self signed cert
distinguished_name = dn

[dn]
countryName = TH
stateOrProvinceName = Bangkok
localityName = City
emailAddress = webmaster@localhost.localhost
commonName = localhost.localhost

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = localhost.localhost
DNS.3 = mydomain.local</code></pre>
<p>ผู้อ่านอาจเปลี่ยนแปลง, ลบ, หรือเพิ่มเติมโดเมนต่างๆที่อยู่ภายใน <code>alt_names</code> ได้ให้สอดคล้องตามที่ต้องการ. จากนั้นใช้คำสั่งต่อไปนี้.</p>
<pre class="line-numbers rd-syntax-highlighter"><code class="language-bash">openssl req -x509 -nodes -newkey rsa:2048 -CA rootCA.crt -CAkey rootCA.key -keyout server.key -days 3650 -out server.crt -config server.cnf</code></pre>
<p>จากคำสั่งด้านบน โปรแกรมจะสร้างไฟล์ server.key, server.crt ทั้งหมดจำนวน 2 ไฟล์.</p>
<h3>นำไปใช้กับ Apache</h3>
<p>สำหรับขั้นตอนเพิ่ม SSL certificate เพื่อนำไปใช้กับเว็บเซิร์ฟเวอร์อย่างเช่น Apache นั้น สามารถอ่านต่อได้ที่ <a href="https://rundiz.com/articles/%e0%b9%80%e0%b8%9b%e0%b8%b4%e0%b8%94%e0%b9%83%e0%b8%8a%e0%b9%89%e0%b8%87%e0%b8%b2%e0%b8%99-ssl-%e0%b8%9a%e0%b8%99-apache">เปิดใช้งาน SSL บน Apache</a>.</p>
<h3>ทำให้เบราว์เซอร์และ Android เชื่อถือ</h3>
<p>ขั้นตอนต่อไปนี้เป็นการทำให้เว็บเบราว์เซอร์ อาทิเช่น Firefox, Google Chrome และระบบปฏิบัติการอื่น เช่น Android เชื่อถือ self-signed certificate ดังกล่าว.</p>
<h4>Google Chrome</h4>
<p>ขั้นตอนต่อไปนี้สำหรับ desktop เท่านั้น</p>
<ol>
<li>เปิดเมนู Settings &gt; Privacy and security &gt; Security &gt; Manage certificates</li>
<li>คลิก Installed by you</li>
<li>คลิก Import ที่แถว Trusted Certificates</li>
<li>เลือกไฟล์ rootCA.crt ที่ได้สร้างไว้ แล้วกด Open</li>
<li>Restart Google Chrome</li>
</ol>
<h4>Firefox</h4>
<p>ขั้นตอนต่อไปนี้สำหรับ desktop เท่านั้น</p>
<ol>
<li>เปิดเมนู Settings &gt; Privacy &amp; Security</li>
<li>คลิกที่ปุ่ม View Certificates</li>
<li>คลิกที่แท็บ Autorities</li>
<li>คลิก Import...</li>
<li>เลือกไฟล์ rootCA.crt ที่ได้สร้างไว้</li>
<li>ติ๊ก trust ทุกอย่างแล้วกด OK</li>
<li>Restart Firefox</li>
</ol>
<h4>Android</h4>
<ol>
<li>ให้ทำการคัดลอกไฟล์ rootCA.crt และ rootCA.key ที่ได้สร้างไว้แล้ว ไปบนอุปกรณ์ Android ด้วยวิธีใดวิธีหนึ่ง</li>
<li>เปิดแอป Settings &gt; Security &gt; Advanced &gt; Encryption &amp; credentials</li>
<li>แตะที่ Install a certificate &gt; CA certificate</li>
<li>แตะที่ Install anyway</li>
<li>เลือกไฟล์ที่นำเข้ามาในขั้นตอนที่ 1 แล้ว</li>
</ol>
<p>The post <a href="https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-self-signed-certificate-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%97%e0%b8%b3%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%9a%e0%b8%a3%e0%b8%b2%e0%b8%a7%e0%b9%8c/">สร้าง self-signed certificate และทำให้เบราว์เซอร์เชื่อถือ</a> appeared first on <a href="https://rundiz.com">rundiz</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rundiz.com/blog/articles/%e0%b8%aa%e0%b8%a3%e0%b9%89%e0%b8%b2%e0%b8%87-self-signed-certificate-%e0%b9%81%e0%b8%a5%e0%b8%b0%e0%b8%97%e0%b8%b3%e0%b9%83%e0%b8%ab%e0%b9%89%e0%b9%80%e0%b8%9a%e0%b8%a3%e0%b8%b2%e0%b8%a7%e0%b9%8c/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
