<?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>How I Built IT | Digital Nomad Blog</title>
	<atom:link href="https://online-dentist.hu/en/category/how-i-built-it/feed/" rel="self" type="application/rss+xml" />
	<link>https://online-dentist.hu</link>
	<description></description>
	<lastBuildDate>Sat, 17 Jan 2026 10:56:16 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://online-dentist.hu/wp-content/uploads/2025/11/cropped-logo_circle_transparent-32x32.png</url>
	<title>How I Built IT | Digital Nomad Blog</title>
	<link>https://online-dentist.hu</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>[10] How I Built a Smart Price and Text Handling System and Dynamic Dropdowns</title>
		<link>https://online-dentist.hu/en/how-i-built-a-smart-price-and-text-handling-system-and-dynamic-dropdowns/</link>
					<comments>https://online-dentist.hu/en/how-i-built-a-smart-price-and-text-handling-system-and-dynamic-dropdowns/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Thu, 01 Jan 2026 05:04:15 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[Javascript]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=5359</guid>

					<description><![CDATA[<p>Dynamic form logic for pairing prices and readable text, cleaning inputs, and auto-selecting single dropdown options for a smoother user experience.</p>
<p>The post <a href="https://online-dentist.hu/en/how-i-built-a-smart-price-and-text-handling-system-and-dynamic-dropdowns/">[10] How I Built a Smart Price and Text Handling System and Dynamic Dropdowns</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<div class="summary-container">
		<p>Summary</p>
		<h2>In this post, I share how I built a dynamic JavaScript dropdown that pairs prices with readable text, auto-selects single options, and keeps inputs clean and consistent.</h2>
	</div>

	
<p>When building complex order forms, sometimes you need more than just numbers.</p>
<p>I wanted a way to store both the price and the readable text for each selected option — clean, trimmed, and paired with its corresponding add-on name. Later, this logic evolved into a dynamic dropdown that automatically selects the only available option.</p>
<p>Here’s how I done the whole thing.</p>
<h2>Step 1: Retrieving the Price and Text from the Selected Option</h2>
<p>It all started with a simple goal:</p>
<p><strong>I can get the price, but I also want the corresponding text.</strong></p>
<p>Initially, my code looked like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">layersPriceSum += parseInt($(this).find('option:selected').data('price'));
layersPriceSumText += parseInt($(this).find('option:selected'));
</pre>
<p>&nbsp;</p>
<p>That second line obviously didn’t work — parseInt() can’t handle a jQuery object, so it returned NaN.</p>
<p>The fix was simple:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">layersPriceSumText += $(this).find('option:selected').text();</pre>
<p>&nbsp;</p>
<p>This gave me the visible text from the dropdown instead of NaN.</p>
<h2>Step 2: Cleaning Up the Text</h2>
<p>Raw .text() can often include unwanted whitespace or line breaks.</p>
<p>So I normalized everything with a single, reliable regex:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">let priceText = $(this).find('option:selected').text().trim().replace(/\s+/g, ' ');</pre>
<p>&nbsp;</p>
<p>Now, no matter how messy the HTML was, my output was always clean: Extra chocolate layer</p>
<h2>Step 3: Including the Add-on Name</h2>
<p>The next step was to make the stored text more descriptive &#8211; not just “Extra large”, but “Chocolate coating – Extra large”.</p>
<p>Each add-on had this structure:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;div class="addon" data-template="szalgyertya" data-price="80"&gt;
  &lt;div class="addon__badge"&gt;
    &lt;span class="addon__title"&gt;Szál gyertya&lt;/span&gt;
  &lt;/div&gt;
  &lt;select&gt;…&lt;/select&gt;
&lt;/div&gt;
</pre>
<p>&nbsp;</p>
<p>So, I extracted the add-on name like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">let addonName = $addon.find('.addon__title').text().trim().replace(/\s+/g, ' ');
let priceText = addonName + ' – ' + $addon.find('option:selected').text().trim().replace(/\s+/g, ' ');</pre>
<p>&nbsp;</p>
<p>That produced clean, contextual strings like: &#8220;Szál gyertya – Extra large&#8221;</p>
<h2>Step 4: Writing the Result into a Hidden Field</h2>
<p>Since I wanted to pass this data through the form without showing it to the user,</p>
<p>I created a hidden &lt;textarea&gt; field:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;textarea name="price_comment" maxlength="1000" style="display:none;"&gt;&lt;/textarea&gt;</pre>
<p>&nbsp;</p>
<p>Then, instead of saving the text to an internal variable, I simply updated the textarea:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">$('textarea[name="price_comment"]').val(sumText);</pre>
<p>&nbsp;</p>
<p>This way, the field stays invisible but still gets submitted with the form &#8211; never use disabled, because those values aren’t sent.</p>
<h2>Step 5: Dynamic Dropdown with Auto-Select</h2>
<p>Next, I built a dynamic &lt;select&gt; that pulls options from a flavors array:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">let options = '&lt;option value=""&gt;Choose a flavor&lt;/option&gt;';

for (const i in flavors) {
  const flavor = flavors[i];
  const price = flavor.price + topDecorationPrice + waferPrice;

  options += '&lt;option data-price="' + price + '" value="' + flavor.variation_id + '"&gt;' +
             flavor.name + ' (' + CakeOrder.formatPrice(price) + ')&lt;/option&gt;';
}

$row.find('select').append(options);</pre>
<p data-start="3645" data-end="3723">If there was <strong data-start="3658" data-end="3677">only one flavor</strong>, it made sense to auto-select it immediately:</p>
<div class="contain-inline-size rounded-2xl corner-superellipse/1.1 relative bg-token-sidebar-surface-primary">
<div class="sticky top-[calc(--spacing(9)+var(--header-height))] @w-xl/main:top-9">
<div class="absolute end-0 bottom-0 flex h-9 items-center pe-2">
<div class="bg-token-bg-elevated-secondary text-token-text-secondary flex items-center gap-4 rounded-sm px-2 font-sans text-xs">
<pre class="EnlighterJSRAW" data-enlighter-language="js">if (flavors.length === 1) {
  $row.find('select').prop('selectedIndex', 1).trigger('change');
  // additional logic could run here
}
</pre>
<p>&nbsp;</p>
</div>
</div>
</div>
</div>
<h2>Step 6: Updating and Displaying the Selection</h2>
<p>Finally, whenever the user picked something, I updated both the total and the readable text:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">$row.find('select').on('change', function() {
  const selected = $(this).find('option:selected');
  const price = parseInt(selected.data('price'));
  const text  = selected.text().trim().replace(/\s+/g, ' ');

  layersPriceSum += price;
  layersPriceSumText += text + '&lt;br&gt;';
});
</pre>
<p>&nbsp;</p>
<p>Everything was clean, readable, and automatic.</p>
<h2>What I Learned</h2>
<ol>
<li>Never parse DOM elements as numbers. .data() is for data, .text() is for humans.</li>
<li>Whitespace cleanup is gold. A simple regex makes dynamic text perfectly consistent.</li>
<li>Hidden fields beat disabled inputs. They submit silently without breaking logic.</li>
<li>Small UX details matter. Auto-selecting the only flavor turns a clunky step into a seamless one.</li>
<li>Readable data structure = maintainable code. Having both price and text stored together simplified later debugging immensely.</li>
</ol>
<h2>Final Thoughts</h2>
<p>This small system turned into a surprisingly powerful and clean workflow — prices, add-on names, and user-readable strings all handled dynamically, safely, and elegantly.</p>
<p>It’s a great example of how improving tiny technical details can make the overall user experience feel polished and professional.</p>
<p>The less the user has to think, the smarter your interface feels.</p><p>The post <a href="https://online-dentist.hu/en/how-i-built-a-smart-price-and-text-handling-system-and-dynamic-dropdowns/">[10] How I Built a Smart Price and Text Handling System and Dynamic Dropdowns</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/how-i-built-a-smart-price-and-text-handling-system-and-dynamic-dropdowns/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[9] How I Built a Dual-Language Email Page Sender Plugin for WordPress</title>
		<link>https://online-dentist.hu/en/how-i-built-a-dual-language-email-page-sender-plugin-for-wordpress/</link>
					<comments>https://online-dentist.hu/en/how-i-built-a-dual-language-email-page-sender-plugin-for-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Fri, 07 Nov 2025 09:37:52 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[Plugin]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4226</guid>

					<description><![CDATA[<p>A lightweight WordPress plugin that lets visitors email the current page’s content to themselves, with built-in SMTP support and automatic bilingual detection.</p>
<p>The post <a href="https://online-dentist.hu/en/how-i-built-a-dual-language-email-page-sender-plugin-for-wordpress/">[9] How I Built a Dual-Language Email Page Sender Plugin for WordPress</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>When I first started this experiment, the prompt was simple:</p>
<blockquote><p>“Is there a WordPress plugin that lets me send the current page’s text to any email entered in a form?”</p></blockquote>
<p>From there, things evolved &#8211; quite a lot.</p>
<p>What began as a theoretical idea turned into a fully functional, bilingual plugin that detects the site language, integrates with SMTP, and securely delivers any page’s content to a recipient’s inbox.</p>
<p>This post documents how that happened.</p>
<h2>Step 1: The Initial Idea</h2>
<p>The original concept was minimalistic:</p>
<ul>
<li>Add a single input field to a WordPress page.</li>
<li>When the visitor enters an email, send the full text of that page to them.</li>
</ul>
<p>At first, I tried to use Contact Form 7 for this, but soon realized it would require too many hooks and workarounds.</p>
<p>A custom plugin would be cleaner &#8211; just one PHP file, one function, no external dependencies.</p>
<h3>Step 2: Building the Core Plugin</h3>
<p>I started by creating a folder inside wp-content/plugins/:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="swift">/wp-content/plugins/email-page-sender/
and added a file named email-page-sender.php.</pre>
<p>&nbsp;</p>
<p>The first working version looked like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;?php
/**
 * Plugin Name: Email Page Sender
 * Description: Sends the current page content to a user-provided email address.
 */

add_action('the_content', function($content) {
    if (is_page()) {
        $form = '
        &lt;form method="post" class="email-page-sender"&gt;
            &lt;input type="email" name="eps_email" placeholder="Enter your email" required&gt;
            &lt;button type="submit" name="eps_submit"&gt;Send&lt;/button&gt;
        &lt;/form&gt;';
        return $content . $form;
    }
    return $content;
});

add_action('template_redirect', function() {
    if (isset($_POST['eps_submit']) &amp;&amp; isset($_POST['eps_email'])) {
        $email = sanitize_email($_POST['eps_email']);
        if (is_email($email)) {
            global $post;
            $subject = 'Page: ' . get_the_title($post-&gt;ID);
            $body = wpautop(apply_filters('the_content', $post-&gt;post_content));
            wp_mail($email, $subject, $body);
        }
    }
});
</pre>
<p>&nbsp;</p>
<p>This minimal version worked &#8211; but only under ideal conditions.</p>
<p>If your server didn’t support PHP mail or lacked SMTP configuration, nothing was actually sent.</p>
<h2>Step 3: Adding SMTP and Proper Headers</h2>
<p>After testing, the first email failed due to Gmail’s DMARC policy:</p>
<blockquote><p>“Unauthenticated email from gmail.com is not accepted&#8230;”</p></blockquote>
<p>That’s when I integrated WP Mail SMTP and switched the “From” address to my domain (noreply@online-dentist.hu).</p>
<p>The fixed mail section became:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">$headers = [
  'Content-Type: text/html; charset=UTF-8',
  'From: Online Dentist &lt;noreply@online-dentist.hu&gt;',
  'Reply-To: schulmann.istvan@gmail.com'
];

$sent = wp_mail($email, $subject, $body, $headers);
</pre>
<p>&nbsp;</p>
<p>Now the email passed DMARC and SPF validation perfectly.</p>
<p>The plugin also displayed a small JavaScript alert confirming success or failure.</p>
<h3>Step 4: Handling Page Context Correctly</h3>
<p>Originally, the plugin hooked into init, but that ran before WordPress loaded the $post object &#8211;<br />
resulting in empty emails.</p>
<p>Switching to template_redirect solved this neatly:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_action('template_redirect', function() {
  // email logic here
});</pre>
<p>&nbsp;</p>
<p>At that point, $post was fully accessible, and the content sent correctly every time.</p>
<h2>Step 5: Adding Dual-Language Support</h2>
<p>My site has both Hungarian and English sections (URLs like /en/&#8230;),<br />
so I wanted the form labels and alerts to match the current language.<br />
To detect the language, I used a simple URL check:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">function eps_get_language() {
    $url = $_SERVER['REQUEST_URI'] ?? '';
    return (strpos($url, '/en/') !== false) ? 'en' : 'hu';
}
</pre>
<p>&nbsp;</p>
<p>Then I used this helper to localize text dynamically:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">$lang = eps_get_language();

if ($lang === 'en') {
    $label = 'Would you like to receive this page by email?';
    $button = 'Send';
} else {
    $label = 'Szeretnéd megkapni ezt az oldalt emailben?';
    $button = 'Küldés';
}
</pre>
<p>&nbsp;</p>
<p>and finally output the localized form:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">&lt;form method="post" class="email-page-sender"&gt;
    &lt;p&gt;&lt;strong&gt;&lt;?php echo esc_html($label); ?&gt;&lt;/strong&gt;&lt;/p&gt;
    &lt;input type="email" name="eps_email" placeholder="Add meg az email címet" required&gt;
    &lt;button type="submit" name="eps_submit"&gt;&lt;?php echo esc_html($button); ?&gt;&lt;/button&gt;
&lt;/form&gt;
</pre>
<p>&nbsp;</p>
<h2>Step 6: Showing Confirmation Alerts</h2>
<p>To make it user-friendly, I added success/error messages injected into the footer:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_action('wp_footer', function() use ($sent, $lang) {
    $msg = $sent
      ? ($lang === 'en' ? '<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> The page content has been sent!' : '<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Az oldal tartalmát elküldtem!')
      : ($lang === 'en' ? '<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Error sending email.' : '<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Hiba történt az email küldése közben.');
    echo "&lt;script&gt;alert(" . json_encode($msg) . ");&lt;/script&gt;";
});
</pre>
<p>&nbsp;</p>
<p>It’s simple, but surprisingly effective.</p>
<h3>What I Learned</h3>
<ul>
<li>Never rely on PHP’s mail() in production.&nbsp;SMTP authentication is a must, especially when using Gmail or custom domains.</li>
<li>DMARC and SPF records matter.&nbsp;Without proper DNS setup, even valid emails may bounce or be marked as spam.</li>
<li>Hook placement in WordPress is critical. init might be too early; template_redirect ensures post data is ready.</li>
<li>Internationalization doesn’t always need heavy plugins. For simple bilingual sites, even URL-based logic can be lightweight and effective.</li>
</ul>
<h3>Final Thoughts</h3>
<p>This started as a quick “can I do this?” experiment &#8211; but became a small, elegant plugin that I’ll actually use on my personal introduction pages.</p>
<p>Sometimes the most satisfying builds aren’t the big, complex systems.</p>
<p>They’re the small, human-focused touches &#8211; like letting someone you just met receive your page with one click.</p>
<p>And that’s exactly what Email Page Sender does: a tiny but polished detail in the bigger story of how I built IT.</p><p>The post <a href="https://online-dentist.hu/en/how-i-built-a-dual-language-email-page-sender-plugin-for-wordpress/">[9] How I Built a Dual-Language Email Page Sender Plugin for WordPress</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/how-i-built-a-dual-language-email-page-sender-plugin-for-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[8] When a Scheduled Cache Task Broke My Sitemaps, and How I Rebuilt the Whole System</title>
		<link>https://online-dentist.hu/en/when-a-scheduled-cache-task-broke-my-sitemaps-and-how-i-rebuilt-the-whole-system/</link>
					<comments>https://online-dentist.hu/en/when-a-scheduled-cache-task-broke-my-sitemaps-and-how-i-rebuilt-the-whole-system/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Fri, 07 Nov 2025 09:16:15 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[Cache control]]></category>
		<category><![CDATA[Cron]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4222</guid>

					<description><![CDATA[<p>Cache preload was not working because Polylang and AIOSEO sitemaps conflicted. The solution: completely disable Google XML Sitemaps and core sitemap.</p>
<p>The post <a href="https://online-dentist.hu/en/when-a-scheduled-cache-task-broke-my-sitemaps-and-how-i-rebuilt-the-whole-system/">[8] When a Scheduled Cache Task Broke My Sitemaps, and How I Rebuilt the Whole System</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2>Step 1: It All Started with a Cron Job</h2>
<div>&nbsp;</div>
<div>The story began with a simple goal:</div>
<div>I wanted the W3 Total Cache preload to automatically rebuild page caches every few hours.</div>
<div>&nbsp;</div>
<div>The preload uses a sitemap as its source, so I set this:</div>
<div>&nbsp;</div>
<div><a href="https://online-dentist.hu/sitemap.xml" target="_blank" rel="noopener">https://online-dentist.hu/sitemap.xml</a></div>
<div>&nbsp;</div>
<div>The cron job was in cPanel:</div>
<div>&nbsp;</div>
<div>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">cd /home/onlinedentist/public_html &amp;&amp; /usr/local/bin/php /home/onlinedentist/public_html/wp-cron.php</pre>
<p>&nbsp;</p>
</div>
<div>…but soon, I noticed something strange: the cache folders were barely filling up &#8211; out of more than 300 multilingual pages, only a handful were cached.</div>
<div>&nbsp;</div>
<div>That was the first clue something deeper was broken.</div>
<div>&nbsp;</div>
<h2>Step 2: The Real Culprit &#8211; AIOSEO, Polylang, and a Broken Sitemap</h2>
<div>&nbsp;</div>
<div>My site runs in two languages using Polylang.</div>
<div>At that time, I also used All in One SEO (AIOSEO), which had its own sitemap system.</div>
<div>When I checked the sitemap index, it looked like this:</div>
<div>&nbsp;</div>
<div>It claimed there were 317 URLs.</div>
<div>But when I clicked one of the sub-sitemaps, there were no items…</div>
<div>&nbsp;</div>
<div>I got this message: “Didn’t expect to see this? Make sure your sitemap is enabled and your content is set to be indexed.”</div>
<div>&nbsp;</div>
<div>So AIOSEO’s sitemap system didn’t actually work with Polylang.</div>
<div>The W3 Total Cache preload relied on it &#8211; and since it was broken, the cache preload had no URLs to warm up.&nbsp;</div>
<div>&nbsp;</div>
<h2>Step 3: Switching to Google XML Sitemaps</h2>
<div>&nbsp;</div>
<div>To fix this, I replaced AIOSEO’s sitemap module with the old but reliable Google XML Sitemaps plugin &#8211; which creates a true, working sitemap.</div>
<div>&nbsp;</div>
<div>This plugin ignores Polylang and lists all pages from all languages, so W3TC finally had a valid source for its preload job.</div>
<div>&nbsp;</div>
<div>After enabling it, my cache folders started filling up properly &#8211; both /hu/ and /en/ pages were cached automatically.</div>
<div>&nbsp;</div>
<h2>Step 4: The Next Problem &#8211; Double Sitemaps</h2>
<div>&nbsp;</div>
<div>Right after fixing that, a new warning appeared in the WordPress dashboard:</div>
<div>&nbsp;</div>
<blockquote>
<div>“One or more plugins are affecting your site’s ability to be indexed.”</div>
</blockquote>
<div>&nbsp;</div>
<div>I checked and realized WordPress itself was still generating its own sitemap.</div>
<div>&nbsp;</div>
<div>Now I had two sitemaps again:</div>
<div>&nbsp;</div>
<ul>
<li>/sitemap.xml (from the plugin, correct)</li>
<li>/wp-sitemap.xml (from WordPress core, unnecessary)</li>
</ul>
<div>&nbsp;</div>
<div>To clean things up, I decided to disable the core sitemap completely.</div>
<div>&nbsp;</div>
<h2>Step 5: Disabling the WordPress Core Sitemap</h2>
<div>&nbsp;</div>
<div>I tried the official filter first:</div>
<div>&nbsp;</div>
<div>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_filter( 'wp_sitemaps_enabled', '__return_false' );</pre>
<p>&nbsp;</p>
</div>
<div>But it didn’t work &#8211; the sitemap stayed alive.</div>
<div>So I added a stronger block in my functions.php:</div>
<div>&nbsp;</div>
<div>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_action( 'init', function() {
    remove_action( 'init', 'wp_sitemaps_get_server' );
    add_filter( 'wp_sitemaps_enabled', '__return_false', 9999 );
}, 0 );
</pre>
<p>&nbsp;</p>
</div>
<div>And to make sure it was completely gone, I placed this .htaccess rule above the WordPress section:</div>
<div>&nbsp;</div>
<div>
<pre class="EnlighterJSRAW" data-enlighter-language="apache"># --- Disable WordPress core sitemap ---
&lt;IfModule mod_rewrite.c&gt;
RewriteEngine On
RewriteCond %{REQUEST_URI} ^/wp-sitemap\.xml$ [OR]
RewriteCond %{REQUEST_URI} ^/wp-sitemap-index\.xml$ [OR]
RewriteCond %{REQUEST_URI} ^/wp-sitemap-posts-[0-9]+\.xml$
RewriteRule .* - [R=404,L]
&lt;/IfModule&gt;</pre>
<p>&nbsp;</p>
</div>
<div>After this, /wp-sitemap.xml finally returned 404 Not Found, while /sitemap.xml continued to work normally.</div>
<div>&nbsp;</div>
<h2>Step 6: Verifying the Robots.txt and Cache Integration</h2>
<div>&nbsp;</div>
<div>Once the sitemap situation was stable,</div>
<div>I checked the virtual robots.txt (generated by WordPress) &#8211; it already contained the correct sitemap reference:</div>
<div>&nbsp;</div>
<div>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml">User-agent: *
Disallow: /wp-admin/
Allow: /wp-admin/admin-ajax.php</pre>
</div>
<div>&nbsp;</div>
<div>No need for a physical robots.txt file.</div>
<div>Then, in W3 Total Cache → Page Cache → Cache Preload, I confirmed:</div>
<div>&nbsp;</div>
<div>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml">Sitemap URL: https://online-dentist.hu/sitemap.xml
Update interval: 1800 seconds
Pages per interval: 10</pre>
<p>&nbsp;</p>
</div>
<div>The next scheduled cron run rebuilt the entire cache successfully.&nbsp;</div>
<div>&nbsp;</div>
<h3>What I Learned</h3>
<div>&nbsp;</div>
<ul>
<li>Scheduled cache preloading only works if the sitemap works.</li>
<li>A broken or plugin-conflicted sitemap silently breaks the preload job.</li>
<li>AIOSEO and Polylang don’t play well together.</li>
<li>The sitemap index looks correct, but the sub-sitemaps are often invalid.</li>
<li>Google XML Sitemaps still works best for multilingual static sitemaps.</li>
<li>It doesn’t overthink language folders &#8211; it just lists every URL.</li>
<li>The WordPress core sitemap must be disabled when using a custom sitemap plugin, or you’ll confuse both Google and W3TC.</li>
<li>Virtual files are fine.</li>
<li>You don’t need physical robots.txt or sitemap.xml &#8211; WordPress can handle them dynamically.</li>
</ul>
<div>&nbsp;</div>
<h3>Final Thoughts</h3>
<div>&nbsp;</div>
<div>This fix began as a routine optimization &#8211; a simple cron job for W3TC &#8211; but it led me deep into WordPress internals, SEO plugin conflicts, and how virtual endpoints really work.</div>
<div>&nbsp;</div>
<div>Now my site has:</div>
<div>&nbsp;</div>
<ul>
<li>One active sitemap (/sitemap.xml)</li>
<li>One dynamic robots.txt</li>
<li>A fully working cache preload system</li>
</ul>
<div>&nbsp;</div>
<div>Sometimes improving performance means stripping away the unnecessary &#8211; not adding more tools, but making sure the ones you already have actually talk to each other.</div><p>The post <a href="https://online-dentist.hu/en/when-a-scheduled-cache-task-broke-my-sitemaps-and-how-i-rebuilt-the-whole-system/">[8] When a Scheduled Cache Task Broke My Sitemaps, and How I Rebuilt the Whole System</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/when-a-scheduled-cache-task-broke-my-sitemaps-and-how-i-rebuilt-the-whole-system/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[7] How I Fixed a Broken WordPress Site When Everything Went Dark</title>
		<link>https://online-dentist.hu/en/how-i-fixed-a-broken-wordpress-site-when-everything-went-dark/</link>
					<comments>https://online-dentist.hu/en/how-i-fixed-a-broken-wordpress-site-when-everything-went-dark/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Mon, 03 Nov 2025 14:52:38 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4179</guid>

					<description><![CDATA[<p>A complete WordPress site crashed with only “Critical error” showing. Step-by-step I revived it by debugging faulty plugins, theme widgets, and PHP 8 issues.</p>
<p>The post <a href="https://online-dentist.hu/en/how-i-fixed-a-broken-wordpress-site-when-everything-went-dark/">[7] How I Fixed a Broken WordPress Site When Everything Went Dark</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Imagine this: one day, a WordPress site you built months ago simply greets you with the message:</p>
<p>There has been a critical error on this website!<br />
<a href="https://online-dentist.hu/wp-content/uploads/2025/11/pi_how_007_critical_error.jpg?x46465"><img fetchpriority="high" decoding="async" class="aligncenter size-full wp-image-4181" src="https://online-dentist.hu/wp-content/uploads/2025/11/pi_how_007_critical_error.jpg?x46465" alt="" width="780" height="159"></a></p>
<p><strong>No WordPress admin.</strong><br />
<strong>No cPanel access.</strong><br />
<strong>No FTP connection.</strong></p>
<p>A great starting point, right? Because when everything collapses, that’s when real debugging begins.</p>
<h3>Step 0: Contact the hosting provider</h3>
<p>The website owner contacted the service provider first. He received this response:</p>
<blockquote><p>You informed us that your website is unavailable, a security update has been made on our web server, unfortunately very old websites may no longer appear this way! Please contact the website developer to update your site! These old PHP-based sites already posed a very high security risk.</p></blockquote>
<p>I joined the conversation here:</p>
<blockquote><p>I am the web developer. It seems that you did not look at the owner&#8217;s website, you just wrote the answer above. Please forgive me if I am wrong! I simply do not understand what you mean when you write in your service provider&#8217;s response that &#8220;these old PHP-based sites&#8230;&#8221; The website &#8211; like your website! &#8211; is a WordPress-based site. It is not an old site at all, although you are right that it is PHP-based, like all WP sites. I assume that you did not mean &#8220;high security risk&#8221; for this type of site either! In my experience, the server&#8217;s security update rarely affects the display of a WordPress site, so your answer is not a reassuring, acceptable answer, but rather raises further questions. If the security update is the cause of the error, I think we have exhausted the concept of service provider risk bearing! <strong>Please, inform me exactly what security update was made? Maybe we can start from this.</strong></p></blockquote>
<p>I tried not to be offensive, but I was still a little surprised by the answer:</p>
<blockquote><p>I apologize, I really didn&#8217;t check the website more thoroughly. We switched from CentOs to Alma Linux and only PHP 8.0 or higher versions are available here, this caused problems for several websites, so I wrote such a template answer. I changed the access password of the site and checked it, I was able to log in to cPanel and access the hosting with FTP.</p></blockquote>
<p>I thanked for the answer and I was able to start working.</p>
<h2>Step 1: Regain Access to the Server</h2>
<p>Since the cPanel and FTP were initially unreachable, I contacted the hosting provider and asked for temporary SSH access. Once in, I navigated to the WordPress directory:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">cd /storage/something/public_html</pre>
<p>&nbsp;</p>
<p>To get the site back online, I needed to disable all plugins manually. The simplest trick:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">mv wp-content/plugins wp-content/plugins_disabled</pre>
<p>&nbsp;</p>
<p>Reload the website &#8211; and boom, the WordPress core loads again (though without plugins).</p>
<h2>Step 2: The First Fatal Error</h2>
<p>Once I could see the original error, it was a PHP fatal error caused by a plugin named tabs-shortcode:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml">Fatal error: Uncaught Error: Non-static method OLT_Tab_Shortcode::add_shortcode() cannot be called statically in tabs-shortcode.php on line 79</pre>
<p>&nbsp;</p>
<p>That means the plugin was trying to call a non-static function statically &#8211; something like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">OLT_Tab_Shortcode::add_shortcode();</pre>
<p>&nbsp;</p>
<p>But it should’ve been called via an instance:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">$olt = new OLT_Tab_Shortcode();
$olt-&gt;add_shortcode();</pre>
<p>&nbsp;</p>
<p>Fixing this (or just removing the plugin) solved the first fatal crash.</p>
<h3>Step 3: The Second Plugin Trap</h3>
<p>After disabling that one, another plugin immediately failed. Same strategy: rename its folder, refresh, and move on.</p>
<p>When dealing with legacy WordPress installs, this “rinse and repeat” cycle is common &#8211; outdated plugins often aren’t compatible with modern PHP 8+.</p>
<p>In this specific case, the provider also improperly prepared one of the 8+ PHP versions, and if this is chosen, the developer may face even more serious errors. I reported this to the provider, but I did not receive a response to this report.</p>
<h3>Step 4: The Theme Takes Over (Meris Widget Error)</h3>
<p>Next, the error shifted to the theme itself:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="lua">Fatal error: Too few arguments to function WP_Widget::__construct(), 0 passed and at least 2 expected</pre>
<p>&nbsp;</p>
<p>This pointed to the Meris theme’s theme-widget.php.<br />
In that file, each widget class looked like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">class meris_home_service_widget extends WP_Widget {
    function meris_home_service_widget() {
        parent::WP_Widget(false, $name = __('Meris: Service', 'meris'));
    }
}
</pre>
<p>&nbsp;</p>
<p>This is outdated PHP 5 code.</p>
<p>Here’s the modernized PHP 8 version:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">class meris_home_service_widget extends WP_Widget {
    function __construct() {
        parent::__construct(
            'meris_home_service_widget',
            __('Meris: Service', 'meris'),
            array('description' =&gt; __('Displays home service items', 'meris'))
        );
    }
}
</pre>
<p>&nbsp;</p>
<p>Once all widget constructors were rewritten, the theme finally loaded… almost.</p>
<h2>Step 5: The Final Boss &#8211; count() TypeError</h2>
<p>Then came this beauty:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml">Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type
Countable|array, string given in index.php on line 76</pre>
<p>&nbsp;</p>
<p>The problem was this snippet:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">$columns = isset($home_sections_array['widget-area-column-item'][$sanitize_areaname])
    ? $home_sections_array['widget-area-column-item'][$sanitize_areaname]
    : "";
$column_num = count($columns);
</pre>
<p>&nbsp;</p>
<p>If $columns wasn’t an array, count() failed.</p>
<p>Here’s the fixed version:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="php">$columns = isset($home_sections_array['widget-area-column-item'][$sanitize_areaname])
    ? $home_sections_array['widget-area-column-item'][$sanitize_areaname]
    : array();

if (!is_array($columns)) {
    $columns = array($columns);
}

$column_num = count($columns);</pre>
<p>&nbsp;</p>
<p>That simple is_array() guard finally stabilized the site.</p>
<h2>Step 6: Clean Up and Prevent Future Chaos</h2>
<p>Once everything worked again:</p>
<ul>
<li>I deleted the broken plugins permanently.</li>
<li>Updated PHP to the latest stable version.</li>
<li>Switched error logging on for future issues:</li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="php">define('WP_DEBUG', true);
define('WP_DEBUG_LOG', true);
define('WP_DEBUG_DISPLAY', false);</pre>
<p>&nbsp;</p>
<p>And finally, I set up automatic offsite backups using a remote cron job &#8211; because no one wants to go through this twice.</p>
<h2>What I Learned</h2>
<ul>
<li>Old WordPress themes often contain ancient PHP 5 code that breaks under PHP 8+.</li>
<li>Static vs. non-static method calls are one of the most common causes of fatal errors in legacy plugins.</li>
<li>Always wrap count() in is_array() when you can’t guarantee the variable type.</li>
<li>Having SSH access is worth gold when FTP and cPanel fail.</li>
<li>The real debugging starts only after you’ve seen five different errors in a row.</li>
</ul>
<h3>Final Thoughts</h3>
<p>Recovering a WordPress site from complete blackout teaches you patience, precision, and humility.</p>
<p>Sometimes, it’s not about rewriting code &#8211; it’s about understanding the story each error tells you.</p>
<p>When your site just says “There has been a critical error on this website!,” take it as an invitation.</p>
<p>The deeper you dig, the more you learn about the invisible architecture holding everything together.</p>
<p>Because as I like to remind myself on How I Built IT:</p>
<span class="aux-highlight aux-highlight-blue">Every bug is a lesson disguised as frustration.</span><p>The post <a href="https://online-dentist.hu/en/how-i-fixed-a-broken-wordpress-site-when-everything-went-dark/">[7] How I Fixed a Broken WordPress Site When Everything Went Dark</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/how-i-fixed-a-broken-wordpress-site-when-everything-went-dark/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[6] Taking WooCommerce Further: When a Popup Window Sends Data Back to the System</title>
		<link>https://online-dentist.hu/en/taking-woocommerce-further-when-a-popup-window-sends-data-back-to-the-system/</link>
					<comments>https://online-dentist.hu/en/taking-woocommerce-further-when-a-popup-window-sends-data-back-to-the-system/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Thu, 30 Oct 2025 15:42:58 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[WooCommerce]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4061</guid>

					<description><![CDATA[<p>A custom WooCommerce feature where a popup window not only displays data but sends it back to the system — redefining how dynamic orders work.</p>
<p>The post <a href="https://online-dentist.hu/en/taking-woocommerce-further-when-a-popup-window-sends-data-back-to-the-system/">[6] Taking WooCommerce Further: When a Popup Window Sends Data Back to the System</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>WooCommerce is a robust, flexible e-commerce engine &#8211; but the real magic happens when we start extending it beyond its standard capabilities.</p>
<p>One of my latest projects did exactly that: I built a dynamic popup window that doesn’t just display information but also communicates directly with WooCommerce, sending data back to the system in real time and influencing the order process itself.</p>
<h3>The Goal</h3>
<p>The goal was to replace a static “extra options” form (for example: cake box, candles, adult extras, etc.) with an interactive popup interface where users could make selections and instantly see updated prices and combinations.</p>
<p>Behind the scenes, JavaScript listens to user interactions and sends updates to the WooCommerce backend via AJAX.</p>
<h2>How It Works</h2>
<p>On the frontend, jQuery monitors each user selection.</p>
<p>When someone chooses a specific cake add-on (like a candle type, color, or decoration), the script:</p>
<ul>
<li>Collects the relevant data- attributes (such as data-price or data-type),</li>
<li>Builds a JSON object from the selection,</li>
<li>Sends it back to the WooCommerce backend via AJAX,</li>
<li>Where a PHP function saves it as custom order meta data.</li>
</ul>
<p>In other words, the popup isn’t just a visual element &#8211; it’s an active UI layer that communicates directly with WooCommerce.</p>
<h3>Key Technologies</h3>
<ul>
<li>JavaScript / jQuery – to manage dynamic elements, events, and value changes.</li>
<li>AJAX <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> PHP bridge – to send and receive data between frontend and backend.</li>
<li>WooCommerce hooks (do_action, add_action) – to integrate data into the checkout and order flow.</li>
<li>Custom order meta fields – to store all additional product information (flavor, size, box type, color, etc.).</li>
</ul>
<h3>The Result</h3>
<p>The final product delivers a truly interactive ordering experience:</p>
<ul>
<li>The customer instantly sees updated prices and options.</li>
<li>Once the popup is confirmed, all selected data is automatically transferred to the cart.</li>
<li>The same data appears in the order confirmation emails and in the WooCommerce admin panel.</li>
</ul>
<p>All of this works fully within WooCommerce’s architecture &#8211; no third-party plugins or core file modifications required.</p>
<h3>Summary</h3>
<p>This project demonstrates that WooCommerce is not just an e-commerce engine &#8211; it’s a powerful development framework.</p>
<p>By leveraging WordPress hooks, WooCommerce actions, JavaScript, and AJAX, we can build experiences that feel seamless, dynamic, and deeply integrated.</p>
<p>Here, the popup isn’t just decoration &#8211; it became a vital part of the data flow itself.</p>
<h2>Code Examples</h2>
<h3>1) Minimal popup UI (HTML)</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="html">&lt;!-- Popup modal (example) --&gt;
&lt;div id="cake-extras-popup" class="popup" hidden&gt;
  &lt;h3&gt;Choose extras&lt;/h3&gt;

  &lt;label&gt;Symbol&lt;/label&gt;
  &lt;select id="jel_gyertya"&gt;
    &lt;option value="sziv"&gt;<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2764.png" alt="❤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Heart&lt;/option&gt;
    &lt;option value="X"&gt;<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2716.png" alt="✖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> X&lt;/option&gt;
    &lt;option value="?"&gt;<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2753.png" alt="❓" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Question&lt;/option&gt;
  &lt;/select&gt;

  &lt;label&gt;Color&lt;/label&gt;
  &lt;select id="jel_gyertya_szin"&gt;
    &lt;option value="csillámos pink"&gt;Glitter Pink&lt;/option&gt;
    &lt;option value="csillámos arany"&gt;Glitter Gold&lt;/option&gt;
    &lt;option value="csillámos ezüst"&gt;Glitter Silver&lt;/option&gt;
  &lt;/select&gt;

  &lt;label&gt;Box&lt;/label&gt;
  &lt;select id="tortadoboz" class="box-selector"&gt;
    &lt;option value="0" data-price="0"&gt;No box&lt;/option&gt;
    &lt;option value="1" data-price="500"&gt;5-slice box (500 Ft)&lt;/option&gt;
    &lt;option value="2" data-price="1000"&gt;10-slice box (1000 Ft)&lt;/option&gt;
  &lt;/select&gt;

  &lt;button id="cake-extras-confirm"&gt;Add to order&lt;/button&gt;
&lt;/div&gt;</pre>
<p>Your real popup can be any UI; what matters is we can read the selections when “Confirm” is clicked.</p>
<h3>2) Enqueue JS and pass AJAX settings (functions.php)</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_action('wp_enqueue_scripts', function () {
    wp_enqueue_script(
        'cake-extras',
        get_stylesheet_directory_uri() . '/js/cake-extras.js',
        ['jquery'],
        '1.0',
        true
    );

    wp_localize_script('cake-extras', 'CakeAjax', [
        'url'   =&gt; admin_url('admin-ajax.php'),
        'nonce' =&gt; wp_create_nonce('cake_extras_nonce'),
    ]);
});</pre>
<p>&nbsp;</p>
<h3>3) Frontend JS: collect → send via AJAX → store in session</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="js">jQuery(function ($) {
  // Example: read price of the selected box
  function getSelectedBoxPrice() {
    const price = $('#tortadoboz option:selected').data('price') || 0;
    return Number(price);
  }

  $('#cake-extras-confirm').on('click', function () {
    const payload = {
      symbol: $('#jel_gyertya').val(),                 // sziv | X | ?
      color:  $('#jel_gyertya_szin').val(),            // e.g. "csillámos arany"
      box:    $('#tortadoboz').val(),                  // 0/1/2...
      box_price: getSelectedBoxPrice(),                // 0 / 500 / 1000
    };

    $.post(CakeAjax.url, {
      action: 'save_cake_extras',
      _ajax_nonce: CakeAjax.nonce,
      data: payload
    }).done(function (res) {
      if (res &amp;&amp; res.success) {
        alert('Extras saved for this order. Now add the product to cart!');
        // Close your modal here.
      } else {
        alert('Could not save extras.');
      }
    });
  });
});</pre>
<p>&nbsp;</p>
<h3>4) PHP AJAX handler: save extras to session</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_action('wp_ajax_save_cake_extras', 'save_cake_extras');
add_action('wp_ajax_nopriv_save_cake_extras', 'save_cake_extras');

function save_cake_extras() {
    check_ajax_referer('cake_extras_nonce');

    if (empty($_POST['data']) || !class_exists('WC_Session')) {
        wp_send_json_error();
    }

    $raw = (array) $_POST['data'];

    // Sanitize
    $extras = [
        'symbol'    =&gt; sanitize_text_field($raw['symbol'] ?? ''),
        'color'     =&gt; sanitize_text_field($raw['color'] ?? ''),
        'box'       =&gt; sanitize_text_field($raw['box'] ?? ''),
        'box_price' =&gt; floatval($raw['box_price'] ?? 0),
    ];

    if (function_exists('WC') &amp;&amp; WC()-&gt;session) {
        WC()-&gt;session-&gt;set('cake_extras', $extras);
        wp_send_json_success();
    }

    wp_send_json_error();
}</pre>
<p>We temporarily keep the popup data in the WooCommerce session until the product is added to the cart.</p>
<h3>5) Inject session data into the cart item (so it travels through checkout)</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="php">// Attach extras when a product is added to the cart
add_filter('woocommerce_add_cart_item_data', function ($cart_item_data, $product_id, $variation_id) {
    if (function_exists('WC') &amp;&amp; WC()-&gt;session) {
        $extras = WC()-&gt;session-&gt;get('cake_extras');
        if (!empty($extras) &amp;&amp; is_array($extras)) {
            $cart_item_data['cake_extras'] = $extras;

            // Optional: add the box price to the line item price
            if (!empty($extras['box_price'])) {
                $cart_item_data['cake_extras']['_adjust_price'] = (float) $extras['box_price'];
            }

            // Clear session so it doesn't leak to another add_to_cart
            WC()-&gt;session-&gt;__unset('cake_extras');
        }
    }
    return $cart_item_data;
}, 10, 3);</pre>
<p>&nbsp;</p>
<h3>6) Adjust the cart line price (optional: if extras add cost)</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_action('woocommerce_before_calculate_totals', function ($cart) {
    if (is_admin() &amp;&amp; !defined('DOING_AJAX')) return;
    if (empty($cart) || empty($cart-&gt;get_cart())) return;

    foreach ($cart-&gt;get_cart() as $cart_item_key =&gt; $item) {
        if (!empty($item['cake_extras']['_adjust_price'])) {
            $extra = (float) $item['cake_extras']['_adjust_price'];
            $price = (float) $item['data']-&gt;get_price('edit');
            $item['data']-&gt;set_price($price + $extra);
        }
    }
});</pre>
<p>&nbsp;</p>
<h3>7) Show extras under the product name in Cart/Checkout</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_filter('woocommerce_get_item_data', function ($item_data, $cart_item) {
    if (empty($cart_item['cake_extras'])) return $item_data;

    $x = $cart_item['cake_extras'];
    if (!empty($x['symbol'])) {
        $item_data[] = [
            'name'  =&gt; __('Symbol', 'your-textdomain'),
            'value' =&gt; esc_html($x['symbol']),
        ];
    }
    if (!empty($x['color'])) {
        $item_data[] = [
            'name'  =&gt; __('Color', 'your-textdomain'),
            'value' =&gt; esc_html($x['color']),
        ];
    }
    if (isset($x['box']) &amp;&amp; $x['box'] !== '') {
        $item_data[] = [
            'name'  =&gt; __('Box', 'your-textdomain'),
            'value' =&gt; esc_html($x['box']),
        ];
    }
    if (!empty($x['box_price'])) {
        $item_data[] = [
            'name'  =&gt; __('Box price', 'your-textdomain'),
            'value' =&gt; wc_price((float) $x['box_price']),
        ];
    }
    return $item_data;
}, 10, 2);</pre>
<p>&nbsp;</p>
<h3>8) Persist extras into the Order Items (so they appear in admin &amp; emails)</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="php">add_action('woocommerce_checkout_create_order_line_item', function ($item, $cart_item_key, $values, $order) {
    if (empty($values['cake_extras'])) return;

    $x = $values['cake_extras'];
    if (!empty($x['symbol'])) {
        $item-&gt;add_meta_data(__('Symbol', 'your-textdomain'), $x['symbol']);
    }
    if (!empty($x['color'])) {
        $item-&gt;add_meta_data(__('Color', 'your-textdomain'), $x['color']);
    }
    if (isset($x['box']) &amp;&amp; $x['box'] !== '') {
        $item-&gt;add_meta_data(__('Box', 'your-textdomain'), $x['box']);
    }
    if (!empty($x['box_price'])) {
        $item-&gt;add_meta_data(__('Box price', 'your-textdomain'), wc_price((float) $x['box_price']));
    }
}, 10, 4);</pre>
<p>WooCommerce will render order item meta in the admin order screen and in customer/admin emails automatically. If you need a custom layout, hook into the email templates (e.g., woocommerce_email_order_meta or override an email template).</p>
<h3>9) (Optional) Toggle an “adults only” label based on selected extras</h3>
<pre class="EnlighterJSRAW" data-enlighter-language="js">jQuery(function ($) {
  $(document).on('change', '.step[data-step="2"] .addon[data-adult="1"] input[type="checkbox"]', function () {
    let hasAdult = false;
    $('.step[data-step="2"] .addon[data-adult="1"] input[type="checkbox"]').each(function () {
      if ($(this).is(':checked')) hasAdult = true;
    });
    window.CakeOrder = window.CakeOrder || {};
    CakeOrder.hasAdultExtra = hasAdult;
    $('#cake-order label.adult')[hasAdult ? 'show' : 'hide']();
  });
});</pre>
<p>&nbsp;</p>
<h2>Why these examples matter</h2>
<ul>
<li>Popup → AJAX → Session keeps the UI responsive while safely handing off data to WooCommerce.</li>
<li>Cart item data + order item meta ensures data survives the entire checkout and lands in emails/admin.</li>
<li>Price adjustment hook keeps the total correct without hacking product data.</li>
<li>No core edits: everything is done via hooks and a tiny script, which is update-safe.</li>
</ul>
<p>&nbsp;</p>
<p>If you want, I can bundle these into a single paste-ready mini-plugin structure so you can drop it into /wp-content/plugins/ and activate.</p><p>The post <a href="https://online-dentist.hu/en/taking-woocommerce-further-when-a-popup-window-sends-data-back-to-the-system/">[6] Taking WooCommerce Further: When a Popup Window Sends Data Back to the System</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/taking-woocommerce-further-when-a-popup-window-sends-data-back-to-the-system/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[5] Dynamic Forms in WordPress: How I Solved the “Add Another Person” Problem</title>
		<link>https://online-dentist.hu/en/dynamic-forms-in-wordpress-how-i-solved-the-add-another-person-problem/</link>
					<comments>https://online-dentist.hu/en/dynamic-forms-in-wordpress-how-i-solved-the-add-another-person-problem/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Thu, 30 Oct 2025 14:55:34 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4056</guid>

					<description><![CDATA[<p>Add dynamic repeatable fields to Contact Form 7 easily with the Repeatable Fields plugin — a clean, GDPR-safe way to collect multiple data entries.</p>
<p>The post <a href="https://online-dentist.hu/en/dynamic-forms-in-wordpress-how-i-solved-the-add-another-person-problem/">[5] Dynamic Forms in WordPress: How I Solved the “Add Another Person” Problem</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>It started with what seemed like a simple need: on a WordPress site I was building, users had to enter their own contact details and optionally add one or more “recommended persons” &#8211; each with a name, phone number, and email address.</p>
<p>Straightforward, right? Until I realized Contact Form 7 &#8211; the plugin I’ve trusted for a decade &#8211; doesn’t natively allow dynamic field groups. Once you fill out a form, that’s it. No “Add another” button. No logic to replicate fields dynamically.</p>
<p>That’s when I went hunting for a better way.</p>
<h3>The Problem</h3>
<p>I wanted a form that would:</p>
<ul>
<li>Collect the sender’s name and email (as personal data).</li>
<li>Let them add multiple recommended persons, each having:
<ul>
<li>Name</li>
<li>Phone number</li>
<li>Email address</li>
</ul>
</li>
<li>Remain fully GDPR-compliant.</li>
<li>Be easy to manage and export (via Flamingo).</li>
</ul>
<p>At first, I considered a JavaScript-based solution that cloned &lt;div&gt; blocks when the last email field was filled in &#8211; but that quickly became messy, especially with validation, IDs, and Flamingo’s data storage.</p>
<h3>The Discovery</h3>
<p>The solution came with a small but powerful plugin:</p>
<p><a href="https://wordpress.org/plugins/cf7-repeatable-fields/" target="_blank" rel="noopener">Contact Form 7 – Repeatable Fields</a>.&nbsp;</p>
<p>This plugin adds a new tag type &#8211; [repeatable] &#8211; which lets you define a group of fields that can be repeated with + / – buttons directly inside your Contact Form 7 editor.</p>
<h3>The Implementation</h3>
<p>Here’s what my setup looked like inside the CF7 form editor:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="raw">[repeatable group-ajanlott min:1 max:5]
  [text* ajanlott_nev placeholder "Ajánlott személy neve"]
  [tel ajanlott_tel placeholder "Ajánlott telefonszám"]
  [email ajanlott_email placeholder "Ajánlott email cím"]
[/repeatable]</pre>
<p>&nbsp;</p>
<p>Just like that, a clean “Add (+)” and “Remove (–)” button appeared.</p>
<p>The plugin handled the field naming, numbering, and submission logic automatically &#8211; no manual JavaScript cloning required.</p>
<p>And to make it visually elegant, I styled the buttons using a few lines of CSS:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="css">/* css */

.wpcf7-field-group-add {
  background: #4f46e5;
  color: #fff;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  font-size: 20px;
  cursor: pointer;
  transition: 0.3s;
}

.wpcf7-field-group-add:hover {
  background: #4338ca;
  transform: translateY(-2px);
}

.wpcf7-field-group-remove {
  background: #ef4444;
  color: #fff;
  border-radius: 50%;
  width: 36px;
  height: 36px;
  font-size: 20px;
  cursor: pointer;
}</pre>
<p>&nbsp;</p>
<p>Simple, clean, and user-friendly.</p>
<h3>Data Handling &amp; GDPR</h3>
<p>Because each “recommended person” involves third-party personal data, I made sure the form also included:</p>
<p>An explicit consent checkbox: “I confirm that the recommended persons have consented to sharing their data for this purpose.”</p>
<p>A dedicated section in the privacy policy describing how these data are handled and stored.</p>
<p>The Flamingo plugin safely logged all submissions, and exporting them to CSV worked flawlessly &#8211; even with multiple repeated field groups.</p>
<h3>The Result</h3>
<p>What started as a frustrating limitation turned into one of my favorite small discoveries in WordPress development this year.</p>
<p>The combination of Contact Form 7 + Repeatable Fields + Flamingo delivers a flexible, privacy-compliant solution for multi-entry forms &#8211; without writing a single line of backend code.</p>
<p>Sometimes, solving a problem isn’t about reinventing the wheel &#8211; it’s about finding the right plugin and shaping it to your workflow.</p>
<h3>Final Thoughts</h3>
<ul>
<li>If your WordPress form ever needs to collect variable-length data (e.g. multiple attendees, passengers, team members, or &#8211; like in my case &#8211; recommended persons), the Contact</li>
<li>Form 7 – Repeatable Fields plugin is an elegant, lightweight answer.</li>
<li>No JavaScript headaches.</li>
<li>No shortcode chaos.</li>
<li>Just a simple “+” button that makes your form smarter.</li>
</ul><p>The post <a href="https://online-dentist.hu/en/dynamic-forms-in-wordpress-how-i-solved-the-add-another-person-problem/">[5] Dynamic Forms in WordPress: How I Solved the “Add Another Person” Problem</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/dynamic-forms-in-wordpress-how-i-solved-the-add-another-person-problem/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[4] How I Built an Excel System to Manage Animal Health Tasks</title>
		<link>https://online-dentist.hu/en/how-i-built-an-excel-system-to-manage-animal-health-tasks/</link>
					<comments>https://online-dentist.hu/en/how-i-built-an-excel-system-to-manage-animal-health-tasks/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Tue, 28 Oct 2025 18:39:39 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[Excel]]></category>
		<category><![CDATA[Visual Basic]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4040</guid>

					<description><![CDATA[<p>I built an Excel-based ERP system to manage animal health programs - automating forecasts, daily logs, and data flow across multiple farms with VBA logic.</p>
<p>The post <a href="https://online-dentist.hu/en/how-i-built-an-excel-system-to-manage-animal-health-tasks/">[4] How I Built an Excel System to Manage Animal Health Tasks</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>When you spend enough time around both farms and spreadsheets, you start seeing them the same way &#8211; full of moving parts, complex dependencies, and a fragile balance between structure and chaos.</p>
<p>This project started as a simple task tracker for animal health programs. It evolved into a full-blown Excel-based ERP-like system, complete with automated forecasting, daily logging, and VBA-driven workflows for multiple farm sites.</p>
<h3>The result &#8211; summary interpretation</h3>
<p>This file is practically a customized ERP mini-system in Excel that:</p>
<ul>
<li>programs and tracks production at the farm level,</li>
<li>automates data collection and logging,</li>
<li>predicts yield and performance,</li>
<li>and connects all data sets (farm, program, hybrid, log).</li>
</ul>
<p>In other words:</p>
<blockquote><p>A macro-supported livestock management system built into Excel, covering everything from planning to daily reporting.</p></blockquote>
<h2>Step 1: Defining the Problem</h2>
<p>Each farm was managing different hybrids &#8211; broilers, layers, growers &#8211; all with their own weekly schedules and health control programs. The biggest challenge was centralizing the data while keeping it flexible enough for local use.</p>
<p>I needed one workbook that could:</p>
<ul>
<li>Handle multiple farms (telephelyek)</li>
<li>Store and forecast production programs</li>
<li>Log daily operational data</li>
<li>Predict outcomes weeks ahead</li>
</ul>
<p>So I built _program.xlsm &#8211; a macro-enabled Excel system that ties together every stage of the production and monitoring process.</p>
<h2>Step 2: Structuring the Workbook</h2>
<p>Every sheet had a clear role in the workflow:</p>
<div class="aux-row aux-col2 aux-tb-col1 aux-mb-col1">
<div class="aux-col"><strong>Sheet</strong> </div>
<div class="aux-col"><strong>Function</strong></div>
<div class="aux-col">Menu The main dashboard</div>
<div class="aux-col">a control center for launching macros and opening program sheets.</div>
<div class="aux-col">Helper</div>
<div class="aux-col">Helper sheet – stores reference data, constants, and validation lists.</div>
<div class="aux-col">Programs</div>
<div class="aux-col">The registry of all active production programs.</div>
<div class="aux-col">Locations</div>
<div class="aux-col">Master list of farms and locations.</div>
<div class="aux-col">Actual program pages</div>
<div class="aux-col">Active program sheets for each farm type.</div>
<div class="aux-col">Prediction </div>
<div class="aux-col">The dynamic forecast, refreshed automatically by VBA.</div>
<div class="aux-col">Diary </div>
<div class="aux-col">The daily data entry sheets where everything comes together.</div>
</div>
<p>What started as a set of templates became a living system that could plan, record, and predict.</p>
<h2>Step 3: Automating the Forecast</h2>
<p>The core of the logic lives inside one macro:</p>
<p>Since I designed the applicable health programs and the templates for each site with a fixed number of rows, sequential processing was given a serious shortcut. Thus, the data for the 30 sites is processed in seconds. The weekly forecast sorts the tasks to be completed and the tasks that have not been completed according to the number of weeks set on the Helper worksheet, starting from the current week.</p>
<pre class="EnlighterJSRAW" data-enlighter-language="visualbasic">‘ Visual Basic

Sub elorejelzes()

    Application.ScreenUpdating = False
    Application.Calculation = xlManual
        
    ' Törlöm az előrejelzés munkalap fejlécen kívüli részét
    sh_elorejelzes.Rows("2:5000").Delete
    hova_irok = 2

    ' Hány hetet nézünk előre
    hetek = sh_seged.Range("Q3").Value
    
    ' a: munkalap ciklus
    For A = 1 To 2
    
        If A = 1 Then
            Set ws = sh_prog_novendek_telep
            meddig = 1363
            tipus = "növendék"
        Else
            Set ws = sh_prog_tojo_telep
            meddig = 683
            tipus = "tojó"
        End If
        
        hanyadik_het_van = ws.Range("A2").Value
        akt_ev = Year(Date)
        
        ' b: a munkalapon meddig vannak sorok
        For b = 4 To meddig
        
            oF = ws.Range("F" &amp; CStr(b)).Value
            
            ' Fejléc meghatározása:
            If (oF = "Telephely:") _
            Then
            
                szerepel_a_tervezesben = ws.Range("Q" &amp; CStr(b + 2)).Value
                
                If szerepel_a_tervezesben = False _
                Then
                    ' Ha nem szerepel a napló a tervezésben, skippelem
                    b = b + 67
                Else
                    telephely = ws.Range("H" &amp; CStr(b)).Value
                    
                    If telephely = "" _
                    Then
                        ' Ha nincs megadva a telephely, skippelem a naplót
                        b = b + 67
                    Else
                    
                        ' Van telephely, szerepel a tervben =&gt; beolvasom a fejléc többi sorát
                        prog_mintavetel = ws.Range("H" &amp; CStr(b + 1)).Value
                        prog_vakcina = ws.Range("H" &amp; CStr(b + 2)).Value
                        istalo_tol = ws.Range("K" &amp; CStr(b)).Value
                        istalo_ig = ws.Range("L" &amp; CStr(b)).Value
                        telepites = ws.Range("K" &amp; CStr(b + 1)).Value
                        allomany_1 = ws.Range("K" &amp; CStr(b + 2)).Value
                        allomany_2 = ws.Range("L" &amp; CStr(b + 2)).Value
                        allomany_3 = ws.Range("M" &amp; CStr(b + 2)).Value
                        het = ws.Range("M" &amp; CStr(b + 1)).Value
                        kor = ws.Range("Q" &amp; CStr(b + 1)).Value
                        
                        'MsgBox telephely
                        
                        ' A napló felső fele a mintavétel
                        program = "mintavétel"
                        
                        For c = b + 6 To b + 66
                        
                            ' Innentől vakcina sorok
                            If c = b + 36 Then program = "vakcina"
                            
                            akt_het = ws.Range("C" &amp; CStr(c)).Value
                            prg_het = ws.Range("M" &amp; CStr(c)).Value
                            teny_datum = ws.Range("Q" &amp; CStr(c)).Value
                            allapot = ws.Range("P" &amp; CStr(c)).Value
                            megjegyzes = ws.Range("R" &amp; CStr(c)).Value
                            
                            
                            ' Ezzel a feléc sorokat skippelem
                            If IsNumeric(akt_het) _
                            Then
                                
                                sor_ev = Year(ws.Range("B" &amp; CStr(c)).Value)
                                
                                kell_a_sor = False
                            
                                ' Ezek már tisztán az adat sorok. A paraméterek alapján döntjük el, hogy szerepel -e az előrejelzésben.
                                If prg_het &gt;= akt_het And prg_het &lt;= akt_het + hetek Then kell_a_sor = True 
                                If teny_datum &lt;&gt; "" Then kell_a_sor = False
                                If prg_het &lt;&gt; 0 And prg_het &lt; hanyadik_het_van And teny_datum = "" And sor_ev &lt;= akt_ev Then kell_a_sor = True
                                
                                If kell_a_sor _
                                Then
                                
                                    ' A fejléc adatok minden sorba bekerülnek:
                                        sh_elorejelzes.Range("A" &amp; CStr(hova_irok)).Value = telephely
                                        sh_elorejelzes.Range("B" &amp; CStr(hova_irok)).Value = istalo_tol
                                        sh_elorejelzes.Range("C" &amp; CStr(hova_irok)).Value = istalo_ig
                                        sh_elorejelzes.Range("D" &amp; CStr(hova_irok)).Value = telepites
                                        sh_elorejelzes.Range("E" &amp; CStr(hova_irok)).Value = "'" &amp; het
                                        sh_elorejelzes.Range("F" &amp; CStr(hova_irok)).Value = allomany_1
                                        sh_elorejelzes.Range("G" &amp; CStr(hova_irok)).Value = allomany_2
                                        sh_elorejelzes.Range("H" &amp; CStr(hova_irok)).Value = allomany_3
                                        sh_elorejelzes.Range("I" &amp; CStr(hova_irok)).Value = prog_mintavetel
                                        sh_elorejelzes.Range("J" &amp; CStr(hova_irok)).Value = prog_vakcina
                                        sh_elorejelzes.Range("K" &amp; CStr(hova_irok)).Value = kor
                                        sh_elorejelzes.Range("L" &amp; CStr(hova_irok)).Value = tipus
                                    
                                    'Az adat sorok másolással kerülnek át:
                                        ws.Range("F" &amp; CStr(c) &amp; ":L" &amp; CStr(c)).Copy
                                        sh_elorejelzes.Range("M" &amp; CStr(hova_irok)).PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks:=False, Transpose:=False
                                        
                                        sh_elorejelzes.Range("T" &amp; CStr(hova_irok)).Value = prg_het
                                        sh_elorejelzes.Range("U" &amp; CStr(hova_irok)).Value = allapot
                                        sh_elorejelzes.Range("V" &amp; CStr(hova_irok)).Value = megjegyzes
                                    
                                    hova_irok = hova_irok + 1
                                
                                
                                End If
                                
                            End If
                        
                        Next c
                        
                        b = b + 67
                        
                    End If
                End If
                 
            End If
        
        Next b
       
    Next A

    Application.ScreenUpdating = True
    Application.Calculation = xlAutomatic

    Call elorejelzes_rendezes(hanyadik_het_van)

    MsgBox "Végeztem az előrejelzéssel."


End Sub</pre>
<p>&nbsp;</p>
<h2>Step 4: The Sorting Issue</h2>
<p>At one point, a simple task &#8211; sorting a list of records &#8211; became the biggest roadblock.</p>
<p>The macro crashed with:</p>
<p><em>“Too few arguments to function”</em></p>
<p>and later:</p>
<p><em>“Argument #1 ($value) must be of type Countable|array, string given.”</em></p>
<p><strong>The culprit?</strong></p>
<p>Merged cells. Excel’s sorting engine simply refuses to work on merged ranges.</p>
<p>After some minutes of debugging, I rewrote the routine:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="visualbasic">‘ Visual Basic

.SortFields.Clear
.SortFields.Add2 _
    Key:=rendezni.Range("L37:L66"), _
    SortOn:=xlSortOnValues, _
    Order:=xlAscending, _
    DataOption:=xlSortNormal
.SetRange rendezni.Range("A37:N66")
.Header = xlNo</pre>
<p>&nbsp;</p>
<p>Once I unmerged the range and used the modern .Add2 method, the problem disappeared.</p>
<p>That fix wasn’t just a patch &#8211; it was a reminder: Excel doesn’t forgive structural shortcuts.</p>
<h2>Step 5: Connecting It All</h2>
<p>After that, the system finally worked seamlessly:</p>
<ul>
<li>New programs can be created from templates</li>
<li>Data flows from daily logs to forecasts</li>
<li>Reports are updated automatically with one click</li>
<li>And every farm’s lifecycle is reflected in a single file</li>
</ul>
<p>It’s not just an Excel file anymore &#8211; <strong>it’s a data-driven management tool.</strong></p>
<h3>What I Learned</h3>
<ul>
<li>Merged cells are the enemy of automation.</li>
<li>VBA macros need a clear separation between data, logic, and output sheets.</li>
<li>Building an ERP-like system in Excel is absolutely possible &#8211; if you respect structure.</li>
<li>Every small bug (like that sorting issue) hides a deeper design lesson.</li>
</ul>
<h3>Final Thoughts</h3>
<p>What started as a weekend experiment became a living ecosystem for animal health management.</p>
<p>This project taught me that even inside the “limited” world of Excel, there’s room for real engineering.</p>
<p>You just need to think like a developer &#8211; and debug like one too.</p>
<span class="aux-highlight aux-highlight-blue">Sometimes the best systems aren’t built with frameworks &#8211; they’re built with patience, logic, and a few thousand cells of pure intent.</span><p>The post <a href="https://online-dentist.hu/en/how-i-built-an-excel-system-to-manage-animal-health-tasks/">[4] How I Built an Excel System to Manage Animal Health Tasks</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/how-i-built-an-excel-system-to-manage-animal-health-tasks/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[3] Setting Up a Visual Calculator in WordPress</title>
		<link>https://online-dentist.hu/en/setting-up-a-visual-calculator-in-wordpress/</link>
					<comments>https://online-dentist.hu/en/setting-up-a-visual-calculator-in-wordpress/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Mon, 27 Oct 2025 17:54:44 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4034</guid>

					<description><![CDATA[<p>Building a custom WordPress price calculator with dynamic logic in Calculated Fields Form—turning a simple idea into a smart, scalable solution.</p>
<p>The post <a href="https://online-dentist.hu/en/setting-up-a-visual-calculator-in-wordpress/">[3] Setting Up a Visual Calculator in WordPress</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>When I first received the task, it sounded straightforward:</p>
<blockquote><p>Place four option buttons on a WordPress page. Each button represents an Ft/m² price. Combine it with a slider that sets the square meters, and display the total cost instantly.</p></blockquote>
<p>That was the initial scope &#8211; simple enough on paper.<br />
But as usual, real development begins where simplicity ends.</p>
<h2>Step 1: From Idea to Specification</h2>
<p>Before writing a single line of code, I turned the initial concept into a development specification.</p>
<p>This meant clarifying every detail with the client &#8211; what exactly needed to happen when each element was changed, and what additional logic would be required behind the scenes.</p>
<p>During this conversation it became clear that we weren’t just calculating price per square meter.</p>
<p>There was another component involved &#8211; a device called the “distributor ring”.</p>
<p>Its quantity also influenced the final price, which meant that the calculator had to integrate an additional pricing table based on the number of these units.</p>
<p>So the project evolved from a two-element formula into a more complex multi-field pricing model, where logic, thresholds, and dynamic values all interacted.</p>
<h3>Step 2: Choosing the Right Plugin</h3>
<p>Initially, I tested three plugins for the job:</p>
<ul>
<li>Cost Calculator Builder</li>
<li>Calculated Fields Form</li>
<li>Forminator</li>
</ul>
<p>The Cost Calculator Builder looked great visually, but as soon as I tried to set up the core calculations, I hit a limitation &#8211; all logic-based features were locked behind the Pro version.</p>
<p>Since the goal was to build a fully functional calculator using only free tools, <strong>I switched to Calculated Fields Form.</strong></p>
<p>This plugin turned out to be incredibly flexible. It allows custom JavaScript formulas, conditional logic, and even user-defined functions &#8211; all within the free tier.</p>
<h2>Step 3: Implementing the Dynamic Logic</h2>
<p>The core logic was simple in concept:</p>
<ul>
<li>Select an option (price per m²)</li>
<li>Set the square meters (via slider)</li>
<li>Define the number of distributor ring units</li>
<li>Multiply, adjust, and display the result instantly</li>
</ul>
<p>At first, the function looked like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Javascript

(function(){
IF(fieldname7&gt;0) return 30500;
IF(fieldname7&gt;2) return 35750;
IF(fieldname7&gt;3) return 40000;
})();</pre>
<p>&nbsp;</p>
<p>It worked &#8211; but it was repetitive, hard to maintain, and far from elegant.<br />
So I restructured it into a clean, scalable JavaScript function that uses threshold bands instead of stacked conditions:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="js">// Javascript

(function () {
var x = +fieldname7 || 0;

var bands = [
{ t: 11, p: 88000 },
{ t: 10, p: 81000 },
{ t: 9, p: 74000 },
{ t: 8, p: 68000 },
{ t: 7, p: 62800 },
{ t: 6, p: 57000 },
{ t: 5, p: 50600 },
{ t: 4, p: 47000 },
{ t: 3, p: 40000 },
{ t: 2, p: 35750 },
{ t: 0, p: 30500 }
];

for (var i = 0; i &lt; bands.length; i++) {
if (x &gt; bands[i].t) return bands[i].p;
}

return 0;
})();</pre>
<p>&nbsp;</p>
<p>This version allowed the calculator to dynamically select the appropriate price based on the “osztókör” count &#8211; effectively turning a static form into a small, reactive pricing engine.</p>
<h2>Step 4: Testing and Fine-Tuning</h2>
<p>Once the logic was implemented, I tested edge cases &#8211; what happens if no option is selected, if the slider is at minimum or maximum, or if invalid input is entered.</p>
<p>The plugin handled it gracefully. The form dynamically updated the total price in real time without page reloads.</p>
<p>With just a few more field connections and a bit of CSS fine-tuning, the calculator was ready to go live.</p>
<h3>What I Learned</h3>
<ul>
<li>Even the simplest-looking calculator hides layers of logic once real client requirements surface.</li>
<li>Calculated Fields Form is a powerful, underrated plugin for free dynamic calculations in WordPress.</li>
<li>Structuring logic into data-driven “bands” (thresholds and prices) makes the code cleaner and far easier to maintain.</li>
<li>Client clarification is key &#8211; turning a vague request into a technical specification prevents confusion later.</li>
</ul>
<h3>Final Thought</h3>
<p>What started as “four option buttons and a slider” turned into a small but satisfying piece of engineering.</p>
<p>It reminded me that in web development, the real skill lies not in complexity &#8211; but in clarity: knowing what the client truly needs, choosing the right tool, and writing logic that feels simple, even when it isn’t.</p>
<figure id="attachment_4035" aria-describedby="caption-attachment-4035" style="width: 1013px" class="wp-caption aligncenter"><a href="https://online-dentist.hu/wp-content/uploads/2025/10/hi_3_price_calculator_in_wordpress.jpg?x46465"><img decoding="async" class="size-full wp-image-4035" src="https://online-dentist.hu/wp-content/uploads/2025/10/hi_3_price_calculator_in_wordpress.jpg?x46465" alt="" width="1013" height="626"></a><figcaption id="caption-attachment-4035" class="wp-caption-text">The final product</figcaption></figure><p>The post <a href="https://online-dentist.hu/en/setting-up-a-visual-calculator-in-wordpress/">[3] Setting Up a Visual Calculator in WordPress</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/setting-up-a-visual-calculator-in-wordpress/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[2] From Cache Chaos to Performance Clarity</title>
		<link>https://online-dentist.hu/en/from-cache-chaos-to-performance-clarity/</link>
					<comments>https://online-dentist.hu/en/from-cache-chaos-to-performance-clarity/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Mon, 27 Oct 2025 17:39:59 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<category><![CDATA[Cache control]]></category>
		<category><![CDATA[Wordpress]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4022</guid>

					<description><![CDATA[<p>Diagnosing cache chaos: from WP Rocket failure to LiteSpeed misfire, finally mastering W3 Total Cache for a fast, stable, and fully preloaded WordPress site.</p>
<p>The post <a href="https://online-dentist.hu/en/from-cache-chaos-to-performance-clarity/">[2] From Cache Chaos to Performance Clarity</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<h2>How I Diagnosed and Fixed a Complex Caching Conflict on a WordPress Website</h2>
<p>When you build websites for a living, you eventually face the same paradox I did: <strong>Your optimization plugin becomes the bottleneck.</strong></p>
<p>That’s exactly what happened to me while optimizing my WordPress site &#8211; online-dentist.hu. This post is a deep-dive case study of how I went from “cache doesn’t work at all” to a fast, stable, and fully automated setup, combining the best of modern WordPress performance practices.</p>
<h3>The Background</h3>
<p>My site was running WP Rocket, one of the most popular speed optimization plugins.<br />
However, despite correct configuration and a valid license, the cache never activated.</p>
<p>The response headers always returned:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># yaml

cache-control: no-cache, no-store, must-revalidate
x-powered-by: PHP/8.4.12</pre>
<p>&nbsp;</p>
<p>The WP Rocket “Cache” tab was missing, and even though the plugin created cache files under<br />
/wp-content/cache/wp-rocket/, the server never served them.<br />
So began a deep technical journey into headers, .htaccess rules, and server-level cache control.</p>
<h2>Step 1: Debugging WP Rocket</h2>
<p>Over several rounds of testing, I:</p>
<ul>
<li>verified headers through DevTools and WebPageTest,</li>
<li>inspected .htaccess, advanced-cache.php, and wp-config.php,</li>
<li>reinstalled WP Rocket cleanly,</li>
<li>and even wrote a custom PHP header plugin to override Cache-Control.</li>
</ul>
<p>Despite all this, the cache remained inactive &#8211; no x-rocket-cache: hit, no static file delivery.</p>
<p>The final clue came from my hosting provider’s support team:</p>
<blockquote><p>“Your server uses Apache. LiteSpeed (Accelerate WP) is available, but the Rocket rewrite rules won’t apply here.”</p></blockquote>
<p>In other words, WP Rocket was operating in PHP-only mode &#8211; optimizing assets, but not caching statically.</p>
<h2>Step 2: Switching to LiteSpeed Cache</h2>
<p>Following the provider’s advice, I activated Accelerate WP, which is essentially LiteSpeed Cache. Initially, I was optimistic &#8211; the plugin integrated well, and PageSpeed scores jumped slightly.</p>
<p>However, when inspecting response headers, I saw:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># yaml

x-litespeed-cache-control: no-cache
x-litespeed-server-type: NONE</pre>
<p>&nbsp;</p>
<p>That second line said it all:</p>
<p>The site wasn’t running on a real LiteSpeed server &#8211; it was still Apache behind the scenes. The plugin worked only as a frontend optimizer, not as a true server-level cache.</p>
<h2>Step 3: The Aha Moment: W3 Total Cache</h2>
<p>So I made a final switch to W3 Total Cache, a plugin I had used many years ago &#8211;<br />
and it instantly clicked (literally).</p>
<p>The response headers finally looked like this:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="yaml"># yaml

cache-control: max-age=3600, public
etag: "44af3fc372e6e07881169370dc86fed2"
last-modified: Wed, 15 Oct 2025 10:50:41 GMT
x-powered-by: W3 Total Cache/2.8.1</pre>
<p>&nbsp;</p>
<p>At last &#8211; proper caching, full gzip compression, and predictable static file handling.</p>
<p><a href="https://www.webpagetest.org/" target="_blank" rel="noopener">WebPageTest</a> confirmed it:</p>
<figure id="attachment_4025" aria-describedby="caption-attachment-4025" style="width: 550px" class="wp-caption aligncenter"><a href="https://online-dentist.hu/wp-content/uploads/2025/10/hi_2_webpage_metrics.jpg?x46465"><img decoding="async" class="size-full wp-image-4025" src="https://online-dentist.hu/wp-content/uploads/2025/10/hi_2_webpage_metrics.jpg?x46465" alt="" width="550" height="117"></a><figcaption id="caption-attachment-4025" class="wp-caption-text">WebPageTest Metrics</figcaption></figure>
<p>&nbsp;</p>
<p>It wasn’t just faster &#8211; it was consistent.</p>
<h2>Step 4: Fixing Mobile and Post Pages</h2>
<p>After celebrating, I noticed a new issue: Blog post pages sometimes failed to load on mobile devices.</p>
<p><strong>The reason?</strong></p>
<p>By default, W3 Total Cache serves a single static HTML version for all devices &#8211; unless you manually enable Mobile User Agent Groups.</p>
<p><strong>Solution</strong>:</p>
<ul>
<li>Navigate to Performance → User Agent Groups.</li>
<li>Enable “Mobile” and “Create a separate cache group.”</li>
<li>Purge all caches.</li>
</ul>
<p>That fixed it instantly &#8211; mobile visitors now get their own cached version, and posts render perfectly.</p>
<h2>Step 5: Preloading the Cache</h2>
<p>By default, W3 builds cache files on first visit, which means the first user after a purge always triggers a slow load.</p>
<p>To automate this, I enabled:</p>
<ul>
<li>Automatic page cache priming (sitemap preload) under Performance → Page Cache</li>
<li>Sitemap: https://online-dentist.hu/sitemap.xml</li>
<li>Interval: every 15 minutes, 20 pages per batch</li>
</ul>
<p>Additionally, I tested WP Warm Cache, a simple but powerful plugin that preloads all URLs in the background.</p>
<p>Now, the cache stays warm 24/7 &#8211; even when no one visits.</p>
<h2>Results &amp; Key Takeaways</h2>
<p>After the final setup:</p>
<ul>
<li>TTFB: 0.4–0.6 s</li>
<li>LCP: &lt; 2.5 s</li>
<li>Speed Index: ~4 s</li>
<li>Mobile PageSpeed: 90+ consistently</li>
<li>Server load: Stable, low CPU usage</li>
</ul>
<h2>What I Learned</h2>
<ul>
<li>Not all cache plugins can work on every server.
<ul>
<li>WP Rocket expects write access and proper .htaccess rewriting &#8211; not always possible on shared Apache hosting.</li>
</ul>
</li>
<li>LiteSpeed Cache ≠ LiteSpeed Server.
<ul>
<li>Unless your host runs LSWS or OLS, the plugin can’t deliver cached HTML.</li>
</ul>
</li>
<li>W3 Total Cache remains a powerhouse
<ul>
<li>especially for Apache-based WordPress sites that need predictable caching and full control.</li>
</ul>
</li>
<li>Always verify with response headers, not assumptions.
<ul>
<li>The truth is in cache-control and x-powered-by.</li>
</ul>
</li>
<li>Preload is everything.
<ul>
<li>A cache is only useful if it’s ready before your visitors arrive.</li>
</ul>
</li>
</ul>
<p><strong>Final Thought</strong></p>
<p>It took patience, dozens of tests, and some friendly frustration, but I finally turned a stubborn WordPress install into a finely tuned, high-performing system.</p>
<p>The takeaway?</p>
<p>Speed doesn’t come from a single plugin &#8211; it comes from understanding how they interact with your server.</p><p>The post <a href="https://online-dentist.hu/en/from-cache-chaos-to-performance-clarity/">[2] From Cache Chaos to Performance Clarity</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/from-cache-chaos-to-performance-clarity/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[1] Reviving a “Lost” Angular Dev Environment on Windows (2025)</title>
		<link>https://online-dentist.hu/en/reviving-a-lost-angular-dev-environment-on-windows-2025/</link>
					<comments>https://online-dentist.hu/en/reviving-a-lost-angular-dev-environment-on-windows-2025/#respond</comments>
		
		<dc:creator><![CDATA[Steve – Digital Nomad]]></dc:creator>
		<pubDate>Sat, 25 Oct 2025 17:50:21 +0000</pubDate>
				<category><![CDATA[How I Built IT]]></category>
		<guid isPermaLink="false">https://online-dentist.hu/?p=4011</guid>

					<description><![CDATA[<p>Reviving an old Angular project on Windows 10: nvm + Node 16, clean npm install, ng serve/build. Remove '!' from paths, add querystring, fix EPERM/Defender.</p>
<p>The post <a href="https://online-dentist.hu/en/reviving-a-lost-angular-dev-environment-on-windows-2025/">[1] Reviving a “Lost” Angular Dev Environment on Windows (2025)</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>When you inherit an Angular project without its original developer-or when you simply come back to an old codebase months later-there’s a special kind of archaeology involved. This post documents how I brought an older Angular app back to life on Windows 10: version wrangling, VS Code setup, npm dependency conflicts, Webpack quirks, and a few very Windows-specific traps.</p>
<p>If you’re facing “it used to work on their machine,” grab a coffee. Here’s the field guide I wish I had.</p>
<h3>The Starting Point</h3>
<p><strong>What I had</strong></p>
<ul>
<li>Source tree: src, node_modules (stale), dist (old build), plus angular.json, package.json, tsconfig*, etc.</li>
<li>Tooling installers mentioned by the previous dev: Node 20.10.0, VS Code 1.85.</li>
<li>Windows 10.</li>
</ul>
<p><strong>What I actually used</strong></p>
<ul>
<li>nvm-windows to manage Node versions.</li>
<li>Node 16.20.2 for maximum compatibility with older Angular/Webpack stacks.</li>
<li>VS Code with Angular Language Service, ESLint, Prettier.</li>
<li>Why Node 16? Older Angular toolchains can be allergic to OpenSSL 3 (Node 18/20). Node 16 is the “safe mode” for legacy builds.</li>
</ul>
<h2>Step 1: Clean, Predictable Tooling</h2>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

# Install and select Node 16
nvm install 16.20.2
nvm use 16.20.2
node -v
npm -v</pre>
<p>&nbsp;</p>
<p>In VS Code, I enabled Format on Save with Prettier and kept ESLint fixes on save. Not strictly required—but little guardrails help.</p>
<h2>Step 2: Fresh Dependencies</h2>
<p>Old node_modules is not a time capsule—it’s drift. I removed it and reinstalled:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

rd -r -fo node_modules 2&gt;$null
del package-lock.json 2&gt;$null
npm install</pre>
<p>&nbsp;</p>
<p>If your first npm install fails with peer dependency noise (mine did: jasmine-core vs karma-jasmine-html-reporter), you have two options:</p>
<ul>
<li>Quick unblock:</li>
</ul>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell">npm install --legacy-peer-deps</pre>
<ul>
<li>Clean fix: bump the offending dev deps to compatible ranges (e.g., jasmine-core &gt;= 3.8).</li>
</ul>
<p>I went “quick unblock” to get the project running, then circled back for tidying.</p>
<h2>Step 3: ng serve… and the Unexpected Traps</h2>
<p><strong>Trap A — The NODE_OPTIONS flag</strong></p>
<p>I initially had &#8211;openssl-legacy-provider baked into npm scripts. With Node 16 that flag is not allowed.<br />
Fix: remove it from package.json scripts and from your shell:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

# package.json scripts → keep it simple
"start": "ng serve -o",
"build": "ng build --configuration=production"

# clear in the current shell
$env:NODE_OPTIONS = $null</pre>
<p>&nbsp;</p>
<p><strong>Trap B — Webpack hates ! in paths</strong></p>
<p>Webpack treats the exclamation mark as loader syntax. My project sat in a folder like:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">A:\!_work\…\…\</pre>
<p>&nbsp;</p>
<p>Fix: move the project to a path without ! or spaces, e.g.:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">A:\work\…\…\</pre>
<p>&nbsp;</p>
<p>Then reinstall once to clear cached absolute paths:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

rd -r -fo node_modules 2&gt;$null
del package-lock.json 2&gt;$null
npm install</pre>
<p>&nbsp;</p>
<p><strong>Trap C — “Can’t resolve &#8216;querystring&#8217;”</strong></p>
<p>Legacy dev-server tried to load querystring (a Node core polyfill no longer auto-provided).</p>
<p>Fix: install the browser-friendly package:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

npm i querystring</pre>
<p>&nbsp;</p>
<p>(If you hit more missing shims like url, buffer, util, add them similarly. In my case only querystring was needed.)</p>
<p>After these, ng serve -o finally launched on http://localhost:4200</p>
<h3>Step 4: Production Build &amp; Windows-ism</h3>
<p>The first prod build succeeded. The second threw:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash"># bash

EPERM: operation not permitted, mkdir '...\dist\SOMETHING'</pre>
<p>&nbsp;</p>
<p>That’s Windows blocking writes (locked folder, Defender’s Controlled Folder Access, sync client, or a stale Node process).</p>
<p>Fix checklist</p>
<p><strong>1. Kill dev server: Ctrl+C (then Y if prompted).</strong></p>
<p>Optionally nuke stray processes:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

taskkill /IM node.exe /F 2&gt;$null</pre>
<p>&nbsp;</p>
<p><strong>2. Remove dist cleanly:</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

cmd /c 'attrib -r -h -s dist\* /s /d' 2&gt;nul
Remove-Item .\dist -Recurse -Force 2&gt;$null</pre>
<p>&nbsp;</p>
<p><strong>3. Rebuild:</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

npm run build</pre>
<p>&nbsp;</p>
<p><strong>4. As a workaround, you can output elsewhere:</strong></p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

npx ng build --configuration=production --output-path .\build-out</pre>
<p>&nbsp;</p>
<p>If Defender’s Ransomware Protection is on, either add node.exe, VS Code, and your project folder as allowed apps/paths, or temporarily disable that protection for the build.</p>
<p>&nbsp;</p>
<h2>Deploy: What to Upload</h2>
<p>Only upload the contents of dist/&lt;project-name&gt;/ (HTML/JS/CSS/assets).</p>
<p>Don’t upload src, node_modules, or config files.</p>
<p>If your site lives under a sub-path (e.g., /app/), build with:</p>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

npx ng build --configuration=production --base-href /app/ --deploy-url /app/</pre>
<p>&nbsp;</p>
<p>If you use Angular Router and direct links 404 on the server, add a rewrite so unknown routes fall back to index.html.</p>
<h3>What I <strong>Learned</strong></h3>
<ul>
<li>Pin a Node LTS that matches your toolchain. Node 16 is a lifesaver for older Angular/webpack stacks.</li>
<li>Paths matter. Avoid special characters (!, ?, #) and spaces in project paths on Windows.</li>
<li>Legacy polyfills are not automatic. If dev-server barks for querystring (or friends), add the package explicitly.</li>
<li>Windows can lock folders. Kill stray Node processes, clear dist, and watch Defender/backup tools.</li>
<li>Keep scripts boring. The fewer environment flags in package.json, the fewer “it works on my shell” mysteries.</li>
</ul>
<p>&nbsp;</p>
<h2>Copy-Paste Quickstart</h2>
<pre class="EnlighterJSRAW" data-enlighter-language="powershell"># Powershell

# 1) Node
nvm install 16.20.2
nvm use 16.20.2
$env:NODE_OPTIONS = $null

# 2) Fresh deps
rd -r -fo node_modules 2&gt;$null
del package-lock.json 2&gt;$null
npm install

# 3) Dev
npm run start # http://localhost:4200

# If 'querystring' error:
npm i querystring
npm run start

# 4) Prod build
# (close dev server first: Ctrl+C)
cmd /c 'attrib -r -h -s dist\* /s /d' 2&gt;nul
Remove-Item .\dist -Recurse -Force 2&gt;$null
npm run build

# 5) Deploy
# Upload the contents of dist/&lt;project-name&gt;/ via FTP/SFTP</pre>
<p>&nbsp;</p>
<h3>Final Thought</h3>
<p>Resurrecting an old Angular app isn’t about luck; it’s about sequencing: pick the right Node, clean your deps, remove magical flags, and keep the path clean. Once you tame those three, most of the “mysterious” build errors disappear—and you can get back to actually building the thing.</p>
<p>&nbsp;</p><p>The post <a href="https://online-dentist.hu/en/reviving-a-lost-angular-dev-environment-on-windows-2025/">[1] Reviving a “Lost” Angular Dev Environment on Windows (2025)</a> first appeared on <a href="https://online-dentist.hu">Digital Nomad Blog</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://online-dentist.hu/en/reviving-a-lost-angular-dev-environment-on-windows-2025/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Object Caching 57/161 objects using Disk
Page Caching using Disk: Enhanced 

Page cache debug info:
Engine:             Disk: Enhanced
Cache key:          online-dentist.hu/en/category/how-i-built-it/feed/_index_slash_ssl.xml
Creation Time:      1779091092.000s
Header info:
Last-Modified:      Sun, 17 May 2026 20:24:39 GMT
ETag:               "2db575ed1c6251c1dbc37b68fcd21248"
X-Powered-By:       W3 Total Cache/2.9.1
X-Robots-Tag:       noindex, follow
Link:               <https://online-dentist.hu/wp-json/>; rel="https://api.w.org/"
Link:               <https://online-dentist.hu/wp-json/wp/v2/categories/513>; rel="alternate"; title="JSON"; type="application/json"
Content-Type:       application/rss+xml; charset=UTF-8

Database Caching 2/48 queries in 0.107 seconds using Disk

Served from: online-dentist.hu @ 2026-05-18 07:58:12 by W3 Total Cache
-->