<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/feeds/stylesheet.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
	
	
	<link href="https://blog.comandeer.pl/feeds/kategorie/javascript.xml" rel="self" type="application/atom+xml"/>
	<link href="https://blog.comandeer.pl/kategorie/javascript" rel="alternate" type="text/html"/>
	<updated>2026-04-25T11:06:04.890Z</updated>
	<id>https://blog.comandeer.pl/feeds/kategorie/javascript.xml</id>
	<title type="html">Comandeerowy blog</title>
	
		<category term="JavaScript"/>
		<subtitle>Kategoria JavaScript</subtitle>
	
	
		
	
	
		<entry>
			<title type="html">Sadząc drzewa</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/sadzac-drzewa" rel="alternate" type="text/html"/>
			<published>2026-01-31T22:06:00.000Z</published>
			<updated>2026-01-31T22:06:00.000Z</updated>
			<id>https://blog.comandeer.pl/sadzac-drzewa</id>
			
				<summary><![CDATA[Narzędzia pozwalają wypluwać przy pomocy JSX-a różne typy drzewek, nie tylko Reactowe. W tym wpisie przyglądam się, jak to zrobić.]]></summary>
			
			<content type="html"><![CDATA[<p>Dawno, dawno temu pokazywałem, jak przygotować <a href="https://blog.comandeer.pl/prymitywna-implementacja-mitycznej-funkcji-react-createelement">własną wersję funkcji <code>React.createElement()</code></a>. Jednak nic więcej z nią nie zrobiliśmy. A przecież możemy ją wykorzystać w JSX-ie!</p>
<h2 id="jsx"><a class="header-anchor" href="https://blog.comandeer.pl/sadzac-drzewa#jsx">JSX</a></h2>
<p>Wypada jednak na samym początku wspomnieć, czym w ogóle jest JSX. Za <a href="https://facebook.github.io/jsx/" rel="noreferrer noopener">oficjalną&nbsp;dokumentacją</a>:</p>
<blockquote>
<p lang="en">JSX is an XML-like syntax extension to ECMAScript without any defined semantics. […] It's intended to be used by various preprocessors (transpilers) to transform these tokens into standard ECMAScript.</p>
<p>[JSX to rozszerzenie składni ECMAScript o składnię podobną do XML-a bez definiowania konkretnej semantyki. […] Jest przeznaczone do użytku przez różnego rodzaju preprocesory (transpilery) do transformacji tych tokenów w standardową składnię ECMAScriptu.]</p>
</blockquote>
<p>Przekładając to na ludzki – JSX pozwala umieszczać w kodzie JS coś, co <em>wygląda</em> jak kod XML:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSX</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> component</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> &lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">article</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Hello, world!&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Some content&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">article</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Niemniej kod w takiej postaci nie ma być rozumiany przez przeglądarki czy środowiska uruchomieniowe typu Node.js. Taki kod powinien być najpierw przepuszczony przez jakieś narzędzie (transpiler), które przetłumaczy go na zwyczajny kod JS. Praktycznie każdy szanujący się&nbsp;transpiler JS-a ma wsparcie dla JSX-a (np. <a href="https://www.typescriptlang.org/docs/handbook/jsx.html" rel="noreferrer noopener">TypeScript ma wbudowane</a>, a <a href="https://babeljs.io/docs/babel-plugin-transform-react-jsx" rel="noreferrer noopener">Babel potrzebuje pluginu</a>). Jeśli przepuścimy przez jeden z nich powyższy kod, powinniśmy otrzymać coś takiego:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> component</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'article'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	React.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'h1'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Hello, world!'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ), </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	React.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'p'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Some content'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Każdy z XML-owych elementów w naszym JSX-ie został zamieniony na wywołanie funkcji <code>React.createElement()</code>. Jest też oddana relacja między poszczególnymi elementami – wywołania dla <code>h1</code> (2) i <code>p</code> (3) znajdują się wewnątrz wywołania dla <code>article</code> (1). Dzięki temu React jest w stanie odtworzyć w swoim vDOM-ie dokładnie takie drzewko, jakie zaprojektowaliśmy w JSX-ie.</p>
<div class="note" role="note" aria-labelledby="note-48"><p class="note__label" id="note-48">Dygresja</p><div class="note__content"><p>Dokładniejszy opis tej funkcji znajduje się w przywołanym już we wstępie <a href="https://blog.comandeer.pl/prymitywna-implementacja-mitycznej-funkcji-react-createelement">artykule o tworzeniu własnej wersji funkcji <code>React.createElement()</code></a>.</p></div></div>
<h2 id="wykorzystanie-własnej-funkcji"><a class="header-anchor" href="https://blog.comandeer.pl/sadzac-drzewa#wykorzystanie-własnej-funkcji">Wykorzystanie własnej funkcji</a></h2>
<p>Domyślnie JSX jest zawsze tłumaczony do wywołań funkcji <code>React.createElement()</code>. A co jeśli nie chcemy korzystać&nbsp;z Reacta? Wówczas mamy dwie możliwości podmiany tej funkcji.</p>
<p>Pierwsza z nich, mniej elegancka, to… stworzenie funkcji <code>React.createElement()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> React</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">		// Tutaj jakiś kod</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Niemniej istnieje też bardziej elegancka metoda: <em>nadpisanie semantyki JSX-a</em>. Czyli powiedzenie naszemu transpilerowi, jak dokładnie ma transformować kod JSX. Minusem tej metody jest różnica w konfiguracji poszczególnych narzędzi. W przypadku jednak tych najpopularniejszych (TypeScript, Babel, <a href="https://esbuild.github.io/api/#jsx-factory" rel="noreferrer noopener">esbuild</a>) można wykorzystać <a href="https://www.johno.com/jsx-pragma" rel="noreferrer noopener">pragmę</a> – czyli komentarz wskazujący odpowiednią funkcję:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSX</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">/* @jsx myFunction */</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> component</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> &lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">article</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Hello, world!&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Some content&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">article</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W tym przypadku informujemy transpiler, że ma używać funkcji o nazwie <code>myFunction()</code>. Dzięki temu po transpilacji dostaniemy następujący kod:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">/* @jsx myFunction */</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> component</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> myFunction</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'article'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	myFunction</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'h1'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Hello, world!'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ),</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	myFunction</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'p'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">null</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Some content'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">);</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Dzięki temu możemy podstawić naszą&nbsp;implementację <code>React.createElement()</code>, która czekała na ten moment <em>niemalże 9 lat</em>:</p>
<div><embed- src="https://codepen.io/Comandeer/pen/NPrYRaw"><p class="embed-fallback"><a href="https://codepen.io/Comandeer/pen/NPrYRaw" rel="noreferrer noopener">Przejdź bezpośrednio do osadzonej treści na CodePenie.</a></p></embed-></div>
<div class="note" role="note" aria-labelledby="note-49"><p class="note__label" id="note-49">Dygresja</p><div class="note__content"><p><a href="https://blog.codepen.io/documentation/es-modules-on-codepen/#react-and-jsx" rel="noreferrer noopener">CodePen pozwala używać Babela</a> do pisania JS-a, dzięki czemu działa na nim JSX.</p></div></div>
<p>Ale wypluwanie elementów HTML z JSX-a jest <em>nudne</em>. Tak naprawdę można generować absolutnie wszystko, co da się&nbsp;zapisać jako drzewko elementów. Jak np. tekstowe drzewko plików:</p>
<div><embed- src="https://codepen.io/Comandeer/pen/JoKLRyg"><p class="embed-fallback"><a href="https://codepen.io/Comandeer/pen/JoKLRyg" rel="noreferrer noopener">Przejdź bezpośrednio do osadzonej treści na CodePenie.</a></p></embed-></div>
<p>W tym przypadku funkcja obsługująca JSX wygląda następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> jsx</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">tag</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">attributes</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#E36209;--shiki-dark:#FFAB70">children</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> attributes; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> childrenContent</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> children.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">child</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> child</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }`</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replaceAll</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#D73A49;--shiki-dark:#F97583">^</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#D73A49;--shiki-dark:#F97583">gm</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#9ECBFF">|   '</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replaceAll</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n\n</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">join</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `| - ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> name</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> childrenContent</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Z JSX-owego elementu pobieramy atrybut <code>name</code> (1) – to nazwa katalogu lub pliku. Zwracamy ją&nbsp;(2) jako ciąg tekstowy razem z fajką (<code>|</code>), wskazującą aktualny poziom zagłębienia, oraz ciągami tekstowymi zwróconymi przez dzieci. Te nieco modyfikujemy, dodając do każdego pliku/katalogu brakujące fajki (3), a następnie łącząc wszystkie dzieci w jeden duży ciąg tekstowy (4).</p>
<p>Natomiast JSX użyty do tego przykładu wygląda następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSX</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">/* @jsx jsx */</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> component</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> &lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">directory</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> name</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">file</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> name</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"README.md"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">directory</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> name</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"src/"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">file</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> name</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"index.ts"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">directory</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">directory</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><h2 id="komponenty"><a class="header-anchor" href="https://blog.comandeer.pl/sadzac-drzewa#komponenty">Komponenty</a></h2>
<p>Niemniej w JSX-ie oprócz zwyczajnych elementów istnieją również komponenty, a więc elementy, których nazwy zaczynają&nbsp;się wielką literą. Spójrzmy na taki przykład:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSX</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> component</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> &lt;</span><span style="color:#005CC5;--shiki-dark:#79B8FF">Greeting</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> name</span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Comandeer"</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> /&gt;;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli przepuścimy ten kod przez transpiler, otrzymamy mniej więcej coś takiego:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> component</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> React.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( Greeting, { name: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Comandeer'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W tym przypadku do <code>React.createElement()</code> jako pierwszy argument nie jest przekazywana nazwa elementu, ale referencja do <code>Greeting</code>. Wypada zatem stworzyć taką funkcję:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Greeting</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#E36209;--shiki-dark:#FFAB70">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> &lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Hello, {name}!&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jako parametr przyjmuje ona obiekt z atrybutami (1), z którego wyciągamy atrybut <code>name</code>. Następnie zwracamy element <code>h1</code>, wewnątrz którego wykorzystujemy ten atrybut do stworzenia powitania (2).</p>
<p>Wypada nieco zmodyfikować naszą funkcję do obsługi JSX-a:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> jsx</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">tag</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">attributes</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#E36209;--shiki-dark:#FFAB70">children</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">typeof</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> tag </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'function'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> tag</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">attributes, children } ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> element</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( tag );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( attributes </span><span style="color:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="color:#D73A49;--shiki-dark:#F97583"> typeof</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> attributes </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'object'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		Object.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">entries</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( attributes ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( [ </span><span style="color:#E36209;--shiki-dark:#FFAB70">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">value</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			element.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">setAttribute</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( name, value );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	children.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">forEach</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">child</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		element.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( child ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> element;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Większość logiki została taka sama, pojawił się jedynie dodatkowy warunek na początku. Sprawdza on, czy przekazany <code>tag</code> jest funkcją&nbsp;(1) i jeśli tak, to wywołuje ją, przekazując jako parametr obiekt zawierający atrybuty oraz własność <code>children</code> z dziećmi (2). Robimy to w taki sposób, żeby zachować Reactowy kształt API. Przy okazji, dzięki użyciu <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/append" rel="noreferrer noopener">metody <code>#append()</code></a> (3) przy dodawaniu dzieci zniknęła potrzeba tworzenia węzła tekstowego. Metoda <code>#append()</code> bowiem, w przeciwieństwie do starszej <a href="https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild" rel="noreferrer noopener">metody <code>#appendChild()</code></a>, pozwala dodawać tekst bezpośrednio do elementu.</p>
<p>Nasza obsługa komponentów prezentuje się następująco:</p>
<div><embed- src="https://codepen.io/Comandeer/pen/ogLqBPm"><p class="embed-fallback"><a href="https://codepen.io/Comandeer/pen/ogLqBPm" rel="noreferrer noopener">Przejdź bezpośrednio do osadzonej treści na CodePenie.</a></p></embed-></div>
<p>I tak oto udało nam się przygotować własną, prostą obsługę JSX-a! Oczywiście, nie jest ona skończona, brakuje choćby sensownej obsługi <a href="https://react.dev/reference/react/Fragment" rel="noreferrer noopener">fragmentów</a>. Niemniej jest fajnym punktem wyjścia do dalszych eksperymentów.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Ale kanał…</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/ale-kanal" rel="alternate" type="text/html"/>
			<published>2025-12-21T23:00:00.000Z</published>
			<updated>2025-12-21T23:00:00.000Z</updated>
			<id>https://blog.comandeer.pl/ale-kanal</id>
			
				<summary><![CDATA[Pora nieco ulepszyć kolejny element bloga – kanały Atom.]]></summary>
			
			<content type="html"><![CDATA[<p>Dzisiaj nadeszła pora na kolejny element bloga, który już od dłuższego czasu czekał na poprawki: kanały Atom.</p>
<h2 id="kanały-atom"><a class="header-anchor" href="https://blog.comandeer.pl/ale-kanal#kanały-atom">Kanały Atom</a></h2>
<p>Dawno, dawno temu ktoś wpadł na szalony pomysł: a co jeśliby pozwolić ludziom czytać treść ze stron internetowych <em>jak i kiedy chcą</em>? Zamiast musieć wchodzić na konkretną&nbsp;stronę WWW, ktoś mógłby po prostu dostać informację o nowym wpisie na blogu czy zdjęciu w galerii podobnie, jak dostaje maila. Ot, taki newsletter, tylko że nie przez maila. Tak powstały tzw. <a href="https://en.wikipedia.org/wiki/Web_feed" rel="noreferrer noopener"><i lang="en">feeds</i> (kanały)</a>. W największym skrócie: kanał to plik w formacie innym niż HTML, w którym znajduje się&nbsp;treść strony i który jest przystosowany do czytania przez tzw. <i lang="en">feed readers</i> (czytniki kanałów). Te czytniki pobierają kanały ze stron co pewien czas, by sprawdzić, czy na stronie pojawiło się coś nowego. Osobiście używam ich jako głównego sposobu na dowiadywanie się o nowościach ze światka webdevu.</p>
<p>Obecnie mamy trzy standardy kanałów: <a href="https://www.rssboard.org/rss-specification" rel="noreferrer noopener"><span lang="en">Really Simple Syndication</span> (RSS)</a>, <a href="https://www.ietf.org/rfc/rfc4287.txt" rel="noreferrer noopener">Atom</a> oraz <a href="https://www.jsonfeed.org/version/1.1/" rel="noreferrer noopener">JSON Feed</a>. Te dwa pierwsze to formaty oparte o XML, podczas gdy ostatni, jak sama nazwa sugeruje, o JSON. Na tym blogu stosuję format Atom.</p>
<h2 id="problemy-z-kanałem"><a class="header-anchor" href="https://blog.comandeer.pl/ale-kanal#problemy-z-kanałem">Problemy z kanałem</a></h2>
<p>Przy okazji <a href="https://blog.comandeer.pl/blog-2.0">odświeżenia bloga</a> dodałem też do nagłówków w postach linki odsyłające do konkretnych sekcji:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">h2</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"odświeżone-bloczki"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 2 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">a</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"header-anchor"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"#odświeżone-bloczki"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Odświeżone bloczki&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">a</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 1 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h2</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zawartość nagłówka jest otoczona linkiem (1), który odsyła do tego nagłówka przy pomocy <a href="https://www.kurshtml.edu.pl/html/do_etykiety,odsylacze.html" rel="noreferrer noopener">kotwicy</a> (2). Problem polega na tym, że atrybut <code>[href]</code> zawiera samą nazwę kotwicy, nie cały URL. A to sprawia, że gdy taki link zostanie wyciągnięty poza oryginalny kontekst (np. do czytnika kanałów), linki w nagłówkach przestają działać. I od dłuższego czasu odkładałem poprawienie tego na później, aż w końcu…</p>
<div class="note" role="note" aria-labelledby="note-61"><p class="note__label" id="note-61">Dygresja</p><div class="note__content"><p>Jeśli zastanawiasz się, czemu wybrałem taki sposób linkowania sekcji, polecam <a href="https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/" rel="noreferrer noopener">artykuł Amber Wilson o dostępnych kotwicach</a>.</p></div></div>
<p>…aż w końcu zmieniłem też bloczki kodu i do problemu z nagłówkami doszedł kolejny:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/ale-kanal/problem-672w.avif"><picture><source type="image/avif" srcset="/assets/images/ale-kanal/problem-440w.avif 440w, /assets/images/ale-kanal/problem-672w.avif 672w" sizes="90vw"><source type="image/webp" srcset="/assets/images/ale-kanal/problem-440w.webp 440w, /assets/images/ale-kanal/problem-672w.webp 672w" sizes="90vw"><source type="image/png" srcset="/assets/images/ale-kanal/problem-440w.png 440w, /assets/images/ale-kanal/problem-672w.png 672w" sizes="90vw"><img src="/assets/images/ale-kanal/problem-672w.png" width="672" height="377" style="aspect-ratio: 672 / 377" alt="Bloczek kodu HTML, nad którym znajduje się tekst &quot;HTML Kopiuj&quot;." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Fragment wpisu <a href="https://blog.comandeer.pl/inkunabuly-madrosci">Inkunabuły mądrości</a> w czytniku kanałów</p></figcaption></figure>
<p>Z racji tego, że czytnik kanałów wycina potencjalnie niebezpieczne elementy, przycisk do kopiowania zawartości bloczków kodu zamienił się w zwykły tekst. Co więcej, nawet gdyby mój czytnik zostawił w tym miejscu przycisk, to i tak by on nie zadziałał, bo nie ma sposobu na dodanie JS-a do kanału Atom. Innymi słowy: kanał mojego bloga miał już dwa niedziałające elementy. Przyszła pora, by coś z tym zrobić!</p>
<h2 id="naprawa-html-a"><a class="header-anchor" href="https://blog.comandeer.pl/ale-kanal#naprawa-html-a">Naprawa HTML-a</a></h2>
<p>Postanowiłem dodać <a href="https://www.11ty.dev/docs/filters/" rel="noreferrer noopener">filtr w Eleventy</a>, który brałby treść posta i wycinał z niego wszystkie elementy, które nie działają w kanale. Problem polega jednak na tym, że w momencie stosowania filtru zawartość posta jest już&nbsp;sparsowana z Markdowna do HTML-a. A to oznacza, że muszę parsować HTML. W tym celu postanowiłem użyć <a href="https://www.npmjs.com/package/cheerio" rel="noreferrer noopener">biblioteki cheerio</a>, która dostarcza API podobnego do <a href="https://jquery.com/" rel="noreferrer noopener">jQuery</a>. Ostatecznie powstał następujący filtr:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { load } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'cheerio'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> MORE_COMMENT_REGEX</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#032F62;--shiki-dark:#DBEDFF">&lt;!--</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\s</span><span style="color:#D73A49;--shiki-dark:#F97583">*</span><span style="color:#032F62;--shiki-dark:#DBEDFF">more</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\s</span><span style="color:#D73A49;--shiki-dark:#F97583">*</span><span style="color:#032F62;--shiki-dark:#DBEDFF">--&gt;</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 10</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">eleventyConfig.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">addFilter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'rss_content'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">post</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postURL</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> cfUrl</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> URL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( post.data.permalink, post.data.site.url ).href ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> $</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> load</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( post.content ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	$</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">':where(h1, h2, h3, h4, h5, h6) a'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">each</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">_</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">link</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> $link</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> $</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( link );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> originalHref</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> $link.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'href'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		$link.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">attr</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'href'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, postURL </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> originalHref ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	$</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'.code__copy'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">remove</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> $</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'body'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">MORE_COMMENT_REGEX</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Dodajemy nowy filtr o nazwie <code>rss_content</code> (1). Przekazujemy mu <a href="https://www.11ty.dev/docs/data-eleventy-supplied/#page-variable" rel="noreferrer noopener">obiekt wpisu</a>. Następnie importujemy <a href="https://cheerio.js.org/docs/api/variables/load" rel="noreferrer noopener">funkcję <code>load()</code></a> z biblioteki <code>cheerio</code> (2). Pozwala ona sparsować kod HTML do DOM-u. Używamy jej do sparsowania zawartości postu (3). Na tak stworzonym drzewku wyszukujemy wszystkie linki w naglówkach (4) i dla każdego z nich pobieramy wartość atrybutu <code>[href]</code> (5) oraz zamieniamy go na absolutny URL (6) – a więc <code>#odświeżone-bloczki</code> zamienią się na <code>https://blog.comandeer.pl/inkunabuly-madrosci#odświeżone-bloczki</code>. Absolutny URL uzyskujemy, sklejając link do posta z linkiem do strony przy pomocy <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/URL" rel="noreferrer noopener">konstruktora <code>URL</code></a> (7). Link do posta jest dostarczony przez Eleventy, natomiast link do strony – <a href="https://github.com/Comandeer/blog/blob/7b5582136be29b149646515c5b355ccb2b6c6b7d/src/_data/site.js#L3" rel="noreferrer noopener">przeze mnie</a> przy pomocy mechanizmu <a href="https://www.11ty.dev/docs/data-global/" rel="noreferrer noopener">globalnych plików z danymi</a>. Po naprawieniu linków, wyszukujemy i <a href="https://cheerio.js.org/docs/api/classes/Cheerio#remove" rel="noreferrer noopener">usuwamy</a> wszystkie przyciski do kopiowania kodu (8). Na koniec <a href="https://cheerio.js.org/docs/api/classes/Cheerio#html" rel="noreferrer noopener">wyciągamy kod HTML</a> z DOM-u i usuwamy z niego komentarz <code>&lt;!--more--&gt;</code> (9) przy pomocy wyrażenia regularnego (10). Mimo usilnych starań nie udało mi się usunąć go w bardziej przyzwoity sposób, używając API biblioteki cheerio. Warto tutaj zwrócić uwagę, że kod HTML wyciągamy z elementu <code>body</code>. Biblioteka cheerio parsuje bowiem HTML zgodnie z regułami opisanymi w specyfikacji, a więc – <a href="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#dom-domparser-parsefromstring" rel="noreferrer noopener">jako pełny dokument</a>, co sprawia, że dodaje brakujące elementy <code>html</code> i <code>body</code>. Można też wymusić na cheerio <a href="https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#fragment-parsing-algorithm-steps" rel="noreferrer noopener">parsowanie w trybie fragmentu</a>, podając <code>false</code> jako trzeci argument do funkcji <code>load()</code>.</p>
<div class="note" role="note" aria-labelledby="note-62"><p class="note__label" id="note-62">Dygresja</p><div class="note__content"><p>Komentarz <code>&lt;!--more--&gt;</code> to technika, którą ukradłem z… <a href="https://codex.wordpress.org/Customizing_the_Read_More" rel="noreferrer noopener">WordPressa</a>. Służy on do oddzielania wstępu artykułu od reszty treści. Wstęp jest widoczny na stronie głównej bloga wraz z linkiem <q>Czytaj więcej</q>, prowadzącym do podstrony z już pełną wersją artykułu.</p></div></div>
<p>Tajemnicza funkcja <code>cfUrl()</code> z poprzedniego fragmentu kodu wycina z przekazanego linku rozszerzenie <code>.html</code> lub fragment <code>index.html</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> cfUrl</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> url.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#032F62;--shiki-dark:#DBEDFF">index</span><span style="color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="color:#032F62;--shiki-dark:#DBEDFF">html</span><span style="color:#D73A49;--shiki-dark:#F97583">$</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="color:#032F62;--shiki-dark:#DBEDFF">html</span><span style="color:#D73A49;--shiki-dark:#F97583">$</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Dzięki temu linki typu <code>/blog-2.0.html</code> lub <code>/kategorie/javascript/index.html</code> zmienią się odpowiednio w <code>/blog-2.0</code> oraz <code>/kategorie/javascript</code>.</p>
<p>Tak stworzony filtr dodajemy następnie do szablonu kanałów, w miejscu, w którym wyświetlana jest treść posta:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Liquid</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">content</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> type</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"html"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;![CDATA[</span><span style="color:#032F62;--shiki-dark:#9ECBFF">{{ post | rss_content }}</span><span style="color:#24292E;--shiki-dark:#E1E4E8">]]&gt;&lt;/</span><span style="color:#B31D28;--shiki-light-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic">content</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>I to w sumie tyle! W końcu kanały bloga powinny działać poprawnie.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Inkunabuły mądrości</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/inkunabuly-madrosci" rel="alternate" type="text/html"/>
			<published>2025-12-20T23:00:00.000Z</published>
			<updated>2025-12-20T23:00:00.000Z</updated>
			<id>https://blog.comandeer.pl/inkunabuly-madrosci</id>
			
				<summary><![CDATA[Poprawiłem bloczki kodu!]]></summary>
			
			<content type="html"><![CDATA[<p>Po odświeżeniu dygresji przyszedł czas na kolejny częsty element mojego bloga: bloczki z kodem!</p>
<h2 id="stare-bloczki"><a class="header-anchor" href="https://blog.comandeer.pl/inkunabuly-madrosci#stare-bloczki">Stare bloczki</a></h2>
<p>Stare bloczki… były:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/inkunabuly-madrosci/stary-bloczek-725w.avif"><picture><source type="image/avif" srcset="/assets/images/inkunabuly-madrosci/stary-bloczek-440w.avif 440w, /assets/images/inkunabuly-madrosci/stary-bloczek-725w.avif 725w" sizes="90vw"><source type="image/webp" srcset="/assets/images/inkunabuly-madrosci/stary-bloczek-440w.webp 440w, /assets/images/inkunabuly-madrosci/stary-bloczek-725w.webp 725w" sizes="90vw"><source type="image/png" srcset="/assets/images/inkunabuly-madrosci/stary-bloczek-440w.png 440w, /assets/images/inkunabuly-madrosci/stary-bloczek-725w.png 725w" sizes="90vw"><img src="/assets/images/inkunabuly-madrosci/stary-bloczek-725w.png" width="725" height="173" style="aspect-ratio: 725 / 173" alt="Bloczek kodu CSS z ciemnego motywu bloga, otoczony białym obramowaniem." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Przykład bloczka kodu z wpisu <a href="https://blog.comandeer.pl/frywolne-marginalia">Frywolne marginalia</a></p></figcaption></figure>
<p>Co prawda spełniały swoje zadanie, miały nawet dobre kolorowanie składni (dzięki <a href="https://shiki.matsu.io/" rel="noreferrer noopener">Shiki</a>), ale brakowało w nich choćby informacji o języku programowania. Dlatego też postanowiłem je nieco odświeżyc.</p>
<h2 id="odświeżone-bloczki"><a class="header-anchor" href="https://blog.comandeer.pl/inkunabuly-madrosci#odświeżone-bloczki">Odświeżone bloczki</a></h2>
<p>Nowe bloczki wyglądają następująco:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/inkunabuly-madrosci/nowy-bloczek-643w.avif"><picture><source type="image/avif" srcset="/assets/images/inkunabuly-madrosci/nowy-bloczek-440w.avif 440w, /assets/images/inkunabuly-madrosci/nowy-bloczek-643w.avif 643w" sizes="90vw"><source type="image/webp" srcset="/assets/images/inkunabuly-madrosci/nowy-bloczek-440w.webp 440w, /assets/images/inkunabuly-madrosci/nowy-bloczek-643w.webp 643w" sizes="90vw"><source type="image/png" srcset="/assets/images/inkunabuly-madrosci/nowy-bloczek-440w.png 440w, /assets/images/inkunabuly-madrosci/nowy-bloczek-643w.png 643w" sizes="90vw"><img src="/assets/images/inkunabuly-madrosci/nowy-bloczek-643w.png" width="643" height="209" style="aspect-ratio: 643 / 209" alt="Bloczek kodu CSS z ciemnego motywu bloga, nad którym znajduje się pasek z nazwą języka programowania oraz przyciskiem &quot;Kopiuj&quot;" loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Przykład odświezonego bloczka kodu z wpisu <a href="https://blog.comandeer.pl/frywolne-marginalia">Frywolne marginalia</a></p></figcaption></figure>
<p>Tak naprawdę zmiany nie są szczególnie wielkie – pojawiła się jedynie nazwa języka oraz przycisk do skopiowania kodu do schowka.</p>
<h3 id="nowy-html"><a class="header-anchor" href="https://blog.comandeer.pl/inkunabuly-madrosci#nowy-html">Nowy HTML</a></h3>
<p>Od strony HTML-a cały bloczek kodu trafił do elementu <code>figure</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">figure</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"code"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> typeof</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"SoftwareSourceCode"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">figcaption</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"code__caption"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 3 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"code__title"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> property</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"programmingLanguage"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;CSS&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 1 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">button</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"code__copy"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Kopiuj&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">button</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 2 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">figcaption</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> class</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"code__code"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> translate</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"no"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> property</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"text"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 4 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">pre</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;[…]&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">code</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">pre</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 5 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">div</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">figure</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Podpis z nazwą języka (1) i przyciskiem (2) został umieszczony w <code>figcaption</code> (3), natomiast sam kod w dodatkowym <code>div</code>ie z <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/translate" rel="noreferrer noopener">atrybutem <code>[translate=no]</code></a> (4). Dzięki temu Google Translate i podobne usługi nie będą próbowały tłumaczyć kodu na inne języki. Dodatkowo zastosowałem też <a href="https://blog.comandeer.pl/o-semantyce-slow-kilka#rozszerzanie-semantyki">RDFa</a>, żeby oznaczyć całość jako kod w języku programowania. Sam kod (5) pozostał bez zmian – dalej jest kolorowany przez Shiki.</p>
<h3 id="nowy-js"><a class="header-anchor" href="https://blog.comandeer.pl/inkunabuly-madrosci#nowy-js">Nowy JS</a></h3>
<p>Przycisk do kopiowania kodu potrzebuje obsługi w JS-ie. Dodałem więc taką do swojego pliku JS:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">addEventListener</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'click'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">evt</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> isCopyButton</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> evt.target.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">closest</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'.code__copy'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">!</span><span style="color:#24292E;--shiki-dark:#E1E4E8">isCopyButton ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> closestCode</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> evt.target.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">closest</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'.code'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ).</span><span style="color:#6F42C1;--shiki-dark:#B392F0">querySelector</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'.code__code'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> navigator.clipboard.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">writeText</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( closestCode.innerText ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Stosuje tutaj technikę <a href="https://javascript.info/event-delegation" rel="noreferrer noopener"><i lang="en">event delegation</i> (delegacji zdarzeń)</a> (1). Dzięki temu mogę przypiąć tylko jeden nasłuchiwacz i obsłużyć wszystkie przyciski “Kopiuj”, zamiast musieć się przypiąć do każdego z nich osobno. Niemniej ta technika sprawia, ze muszę sprawdzić, czy na pewno został kliknięty przycisk “Kopiuj”. Robię to przy pomocy wyszukania go przez <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/closest" rel="noreferrer noopener">metodę <code>#closest()</code></a> (2). Metoda ta zwróci najbliższy element o klasie <code>.code__copy</code>. W przypadku, gdy ktoś kliknie przycisk, będzie to on sam. Metoda <code>#closest()</code> zabezpiecza nas przed przypadkiem, gdybyśmy dodali kiedyś ikonkę do przycisku i ktoś kliknął w nią – wówczas zamiast przycisku mielibyśmy w <code>evt.target</code> np. element <code>svg</code>. Jak już ustalimy, że został naciśnięty odpowiedni przycisk, wyszukujemy najbliższy bloczek kodu (3). Następnie wywołujemy <a href="https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText" rel="noreferrer noopener">metodę <code>Clipboard#writeText()</code></a>, do której przekazujemy tekstową zawartość bloczka (4). Jeśli osoba naciskająca przycisk wyrazi zgodę na zapisanie danych do schowka, kod zostanie skopiowany.</p>
<h3 id="nowy-backend"><a class="header-anchor" href="https://blog.comandeer.pl/inkunabuly-madrosci#nowy-backend">Nowy backend</a></h3>
<p>Nowe bloczki kodu renderowane są przy pomocy autorskiego pluginu do <a href="https://www.npmjs.com/package/markdown-it" rel="noreferrer noopener">MarkdownIt</a> (biblioteki renderującej Markdowna w Eleventy). Jego kod prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> markdownItCodeBlock</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">markdownIt</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> originalFenceRule</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> markdownIt.renderer.rules.fence; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	markdownIt.renderer.rules.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">fence</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">tokens</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">idx</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">options</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">env</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">slf</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> token</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> tokens[ idx ];</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> lang</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> token.info </span><span style="color:#D73A49;--shiki-dark:#F97583">?</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> token.info.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() </span><span style="color:#D73A49;--shiki-dark:#F97583">:</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> renderedCodeBlock</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> originalFenceRule</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( tokens, idx, options, env, slf ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		/</span><span style="color:#D73A49;--shiki-dark:#F97583"> *</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 5</span><span style="color:#D73A49;--shiki-dark:#F97583"> */</span><span style="color:#D73A49;--shiki-dark:#F97583"> return</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `&lt;figure class="code" typeof="SoftwareSourceCode"&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			&lt;figcaption class="code__caption"&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				&lt;span class="code__title" property="programmingLanguage"&gt;${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> langs</span><span style="color:#032F62;--shiki-dark:#9ECBFF">[ </span><span style="color:#24292E;--shiki-dark:#E1E4E8">lang</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ] </span><span style="color:#D73A49;--shiki-dark:#F97583">??</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }&lt;/span&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				&lt;button class="code__copy"&gt;Kopiuj&lt;/button&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			&lt;/figcaption&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			&lt;div class="code__code" translate="no" property="text"&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> renderedCodeBlock</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			&lt;/div&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		&lt;/figure&gt;`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku zapisujemy sobie do zmiennej aktualną funkcję do renderowania bloczków kodu (1). W przypadku MarkdownIt funkcja ta znajduje się w <a href="https://markdown-it.github.io/markdown-it/#Renderer.prototype.rules" rel="noreferrer noopener">regułach renderera</a>. Następnie nadpisujemy tę regułę własną (2). Wyciągamy z kodu Markdown nazwę języka programowania (3), a następnie wywołujemy oryginalną regułę renderującą bloczki kodu (4). Dzięki temu dostajemy kod ładnie pokolorowany przez Shiki. Na koniec  tworzymy nasz własny kod HTML bloczka (5), do którego wsadzamy pokolorowany kod i zwracamy całość&nbsp;z funkcji.</p>
<p>Teraz zostaje tylko dodać ten plugin do <a href="https://github.com/Comandeer/blog/blob/983e2db060f90d4bcfc923187c2147d3c937e12d/plugins/markdownIt.js#L113" rel="noreferrer noopener">naszej zmodyfikowanej wersji MarkdownIt</a>. Można to zrobić przy pomocy <a href="https://markdown-it.github.io/markdown-it/#MarkdownIt.use" rel="noreferrer noopener">metody <code>#use()</code></a>.</p>
<p>I to tyle! Bloczki kodu na blogu są od teraz <em>nieco</em> lepsze.</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Ufam ci!</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/ufam-ci" rel="alternate" type="text/html"/>
			<published>2025-12-18T23:00:00.000Z</published>
			<updated>2025-12-18T23:00:00.000Z</updated>
			<id>https://blog.comandeer.pl/ufam-ci</id>
			
				<summary><![CDATA[HTML Sanitizer API to tylko jedna z metod obrony przed XSS w przeglądarkach. Drugą jest Trusted Types API.]]></summary>
			
			<content type="html"><![CDATA[<p><a href="https://blog.comandeer.pl/dezynfekcja">HTML Sanitizer API</a> jest dobrą i skuteczną&nbsp;odpowiedzią na problem ataków XSS. Ma jednak jedną, zasadniczą wadę: jego dodanie do już istniejących aplikacji wymaga ingerencji w kod. I to często w wielu miejscach. Na szczęście przeglądarki udostępniają jeszcze jedno API, które sprawdzi się o wiele lepiej w takiej sytuacji.</p>
<h2 id="problem-z-istniejącymi-aplikacjami"><a class="header-anchor" href="https://blog.comandeer.pl/ufam-ci#problem-z-istniejącymi-aplikacjami">Problem z istniejącymi aplikacjami</a></h2>
<p>Żeby zabezpieczyć aplikację przy pomocy HTML Sanitizer API, trzeba zadbać o to, żeby wszystkie miejsca, w których dodajemy HTML, korzystały z nowej <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML" rel="noreferrer noopener">metody <code>#setHTML()</code></a>. Innymi słowy: wszystkie wystąpienia <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML" rel="noreferrer noopener">własności <code>#innerHTML</code></a> trzeba podmienić. I choć na pierwszy rzut oka nie wydaje się to specjalnie skomplikowane, to z powodu złożoności aplikacji może to nastręczyć sporo problemów.</p>
<p>Pierwszym z nich jest zmiana zachowania aplikacji. Dotąd HTML był ustawiany bez dodatkowego czyszczenia. Teraz jest filtrowany. W zależności od aplikacji, może to być problemem. Weźmy takiego <a href="https://ckeditor.com/" rel="noreferrer noopener">CKEditora</a>. Jeśli nagle przestanie działać dodawanie obrazków w edytorze (bo HTML Sanitizer APi je wytnie), to on sam stanie się średnio użyteczny. Stąd proste podmienienie własności <code>#innerHTML</code> na metodę <code>#setHTML()</code> w takim wypadku nie zadziała.</p>
<p>Drugi z problemów związany jest z samym refactorem. Bo to prawie nigdy nie jest po prostu podmiana czegoś w kodzie. Często trzeba też zmienić choćby testy, które mogą zacząć padać po zmianie zachowania aplikacji. Często też trzeba sprawdzić, czemu testy <em>nie</em> zaczęły padać – by się upewnić, że na pewno wszystko dobrze testują.</p>
<p>Trzeci problem to zależności aplikacji. Nasz kod możemy w ten czy inny sposób zrefactoryzować. Ale z zależnościami nie jest już&nbsp;tak łatwo i najczęściej kończy się na czekaniu, aż dany projekt doda wsparcie dla nowego API. A na to se można poczekać – o ile dany projekt w ogóle będzie chciał taką zmianę wprowadzić.</p>
<p>I wreszcie: kompatybilność. Dopóki HTML Sanitizer API nie będzie miało sensownego wsparcia we wszystkich najważniejszych przeglądarkach, trzeba będzie zadbać o alternatywę. Najprawdopodobniej w postaci <a href="https://developer.mozilla.org/en-US/docs/Glossary/Polyfill" rel="noreferrer noopener">polyfilla</a>. A to dodatkowo niepotrzebnie skomplikuje kod i dołoży kolejną&nbsp;zależność do projektu.</p>
<p>Innymi słowy: wdrożenie HTML Sanitizer API może być sporym wyzwaniem w istniejących projektach.</p>
<h2 id="trusted-types-api"><a class="header-anchor" href="https://blog.comandeer.pl/ufam-ci#trusted-types-api">Trusted Types API</a></h2>
<p>Na szczęście istnieje inne API, które ma zdecydowanie lepsze wsparcie i które można wpiąć w już istniejące aplikacje przy minimalnej ingerencji. To API to <a href="https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API" rel="noreferrer noopener"><i lang="en">Trusted Types API</i> (API Zaufanych Typów)</a>. Pozwala ono zabezpieczyć tzw. <i lang="en">injection sinks</i> (miejsca wstrzykiwania) przy pomocy polityk.</p>
<h3 id="miejsca-wstrzykiwania"><a class="header-anchor" href="https://blog.comandeer.pl/ufam-ci#miejsca-wstrzykiwania">Miejsca wstrzykiwania</a></h3>
<p>Miejsca wstrzykiwania to wszystkie te fragmenty kodu, w których do strony WWW jest dodawana treść lub może być wykonany skrypt JS. Najprostszym przykładem jest własność&nbsp;<code>#innerHTML</code>, przy pomocy której można wsadzić HTML do wybranego elementu. Są też jednak mniej oczywiste przykłady:</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/text" rel="noreferrer noopener">własność <code>HTMLScriptElement#text</code></a> – służąca do zmiany zawartości elementu <code>script</code>,</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLScriptElement/src" rel="noreferrer noopener">własność <code>HTMLScriptElement#src</code></a> – służąca do zmiany źródła elementu <code>script</code>,</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval" rel="noreferrer noopener">funkcja <code>eval()</code></a> – wykonująca przekazany jej kod JS,</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString" rel="noreferrer noopener">metoda <code>DOMParser#parseFromString()</code></a> – parsująca przekazany kod HTML,</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTMLUnsafe" rel="noreferrer noopener">metoda <code>setHTMLUnsafe()</code></a> – służąca do zmiany zawartości elementu z pominięciem czyszczenia przekazanego kodu.</li>
</ul>
<p>Takie przykłady można by mnożyć. MDN zawiera sporą <a href="https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API#injection_sink_interfaces" rel="noreferrer noopener">listę potencjalnych miejsc wstrzykiwania</a>.</p>
<h3 id="polityki"><a class="header-anchor" href="https://blog.comandeer.pl/ufam-ci#polityki">Polityki</a></h3>
<p>Trzonem Trusted Types API są tzw. polityki. To obiekty zawierające konfigurację opisują, w jaki sposób mają być zabezpieczone poszczególne miejsca wstrzykiwania. Każda polityka może składać się z trzech <a href="https://refactoring.guru/design-patterns/factory-method" rel="noreferrer noopener">metod-fabryk</a>:</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy/createHTML" rel="noreferrer noopener"><code>#createHTML()</code></a> – służąca do tworzenia <a href="https://developer.mozilla.org/en-US/docs/Web/API/TrustedHTML" rel="noreferrer noopener">obiektów <code>TrustedHTML</code></a>, które mogą być później używane wszędzie tam, gdzie trzeba przekazać kod HTML, np. własność <code>#innerHTML</code> lub metoda <code>#setHTMLUnsafe()</code>,</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy/createScript" rel="noreferrer noopener"><code>#createScript()</code></a> – służąca do tworzenia <a href="https://developer.mozilla.org/en-US/docs/Web/API/TrustedScript" rel="noreferrer noopener">obiektów <code>TrustedScript</code></a>, które mogą być później używane tam, gdzie jest potrzebny kod JS, np. wewnątrz funkcji <code>eval()</code>,</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy/createScriptURL" rel="noreferrer noopener"><code>#createScriptURL()</code></a> – służąca do tworzenia <a href="https://developer.mozilla.org/en-US/docs/Web/API/TrustedScriptURL" rel="noreferrer noopener">obiektów <code>TrustedScriptURL</code></a>, które mogą być później używane tam, gdzie potrzebny jest adres skryptu JS, np. we własności <code>HTMLScriptElement#src</code>.</li>
</ul>
<p>Żeby dodać nową politykę, trzeba skorzystać z <a href="https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicyFactory/createPolicy" rel="noreferrer noopener">metody <code>trustedTypes#createPolicy()</code></a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> policy</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> trustedTypes.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createPolicy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'my-policy'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	createHTML</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sanitizeHTML</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( html );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	createScript</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">js</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sanitizeJS</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( js );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	createScriptURL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sanitizeURL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( url );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tworzymy nową politykę o nazwie <code>my-policy</code> i zapisujemy ją do zmiennej <code>policy</code> (1). Ma ona wszystkie trzy metody – <code>#createHTML()</code> (2), <code>#createScript()</code> (3) oraz <code>#createScriptURL()</code> (4). Zwracają one przekazany im kod przepuszczony przez odpowiednie funkcje filtrujące (5, 6, 7 ). To, co te funkcje robią, zależy w zupełności od nas. W końcu każda aplikacja jest inna i może mieć inne wymagania odnośnie czyszczenia HTML-a czy JS-a. Na potrzeby przykładu stwórzmy zaślepki, które po prostu zwrócą przekazany im argument:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sanitizeHTML</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> html;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sanitizeJS</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">js</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> js;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sanitizeURL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> url;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Żeby wykorzystać w praktyce naszą politykę, trzeba wywołać jej metody:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> div</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'div'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">div.innerHTML </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> policy.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createHTML</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'&lt;p&gt;Jestem zaufanym HTML-em&lt;/p&gt;'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">eval</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( policy.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createScript</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'alert(1)'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> script</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'script'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">script.src </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> policy.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createScriptURL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'data:text/javascript;charset=utf-8;base64,YWxlcnQoMSk='</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tworzymy element <code>div</code> (1), a następnie wsadzamy do niego “zaufany” HTML, stworzony przy pomocy metody <code>policy#createHTML()</code> (2). Z kolei do funkcji <code>eval()</code> przekazujemy “zaufany” skrypt, stworzony przy pomocy metody <code>policy#createScript()</code> (3). Na koniec tworzymy element <code>script</code> (4) i ustawiamy jego własność <code>#src</code> na “zaufany” URL skryptu stworzony przy pomocy metody <code>policy#createScriptURL()</code> (5).</p>
<h3 id="wymuszanie-polityki"><a class="header-anchor" href="https://blog.comandeer.pl/ufam-ci#wymuszanie-polityki">Wymuszanie polityki</a></h3>
<p>Samo jednak dodanie polityki nie zabezpieczy aplikacji. Przeglądarka nie będzie miała żadnego problemu z tym, żeby ją ominąć i ustawić własność <code>#innerHTML</code> na dowolny tekst. Dlatego API dostarcza też mechanizm, który wymusza stosowanie polityki. Służy do tego <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/require-trusted-types-for" rel="noreferrer noopener">dyrektywa <code>require-trusted-types-for</code></a> w <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy" rel="noreferrer noopener">nagłówku HTTP <code>Content-Security-Policy</code></a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>Content-Security-Policy: require-trusted-types-for 'script';</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Jak na razie jedyną wartością dla tej dyrektywy jest <code>'script'</code>, która pokrywa wszystkie trzy przypadki (kod HTML, kod JS i URL-e skryptów).</p>
<p>Jeśli wyślemy taki nagłówek HTTP, a następnie spróbujemy wstawić niezabezpieczony tekst do własności <code>#innerHTML</code>, powinniśmy otrzymać następujący błąd:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>This document requires 'TrustedHTML' assignment. The action has been blocked.</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Istnieje też <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/trusted-types" rel="noreferrer noopener">dyrektywa <code>trusted-types</code></a>, która kontroluje tworzenie polityk:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>Content-Security-Policy: require-trusted-types-for 'script';trusted-types my-policy;</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Taki nagłówek wymusi używanie polityki, a równocześnie pozwoli stworzyć jedynie taką o nazwie <code>my-policy</code>.</p>
<h3 id="domyślna-polityka"><a class="header-anchor" href="https://blog.comandeer.pl/ufam-ci#domyślna-polityka">Domyślna polityka</a></h3>
<p>Ale zaraz, zaraz – czy to API nie miało być tym “mniej inwazyjnym”, które nie wymaga sporego refactoru istniejących aplikacji? Takie tworzenie polityk wydaje się wręcz jeszcze bardziej upierdliwe, niż prosta zamiana <code>#innerHTML</code> na <code>#setHTML()</code>. Na całe szczęście Trusted Types API ma asa w rękawie: domyślną politykę. Jeśli wymusimy stosowanie polityk przy pomocy dyrektywy <code>require-trusted-types-for</code>, a następnie stworzymy politykę o nazwie <code>default</code>, będzie ona automatycznie wykorzystywana w każdym miejscu wstrzykiwania:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">trustedTypes.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createPolicy</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'default'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	createHTML</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'HTML'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> html; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	createScript</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">js</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'JS'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> js; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">	createScriptURL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">url</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'URL'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> url; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Tworzymy nową politykę o nazwie <code>'default'</code> (1). Ponownie tworzymy metody-fabryki, w których zwracamy przekazany im kod (2, 3, 4). Wcześniej jednak logujemy w konsoli, jaki typ danych filtrujemy (5, 6, 7)</p>
<p>Wykorzystanie tej polityki jest całkowicie przezroczyste:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> div</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'div'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">div.innerHTML </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '&lt;p&gt;Jestem zaufanym HTML-em&lt;/p&gt;'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">eval</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'alert(1)'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> script</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'script'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">script.src </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'data:text/javascript;charset=utf-8;base64,YWxlcnQoMSk='</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Nie trzeba się już odwoływać do polityki ani przy przypisywaniu wartości do własności <code>#innerHTML</code> (1), ani przy wywoływaniu funkcji <code>eval()</code> (2), ani przy ustawianiu źródła skryptu (3).</p>
<p>Jeśli odpalimy ten kod w przeglądarce, wówczas powinniśmy zobaczyć w konsoli logi potwierdzające, że polityka została zaaplikowana do każdego z trzech miejsc wstrzykiwania.</p>
<h3 id="kompatybilność"><a class="header-anchor" href="https://blog.comandeer.pl/ufam-ci#kompatybilność">Kompatybilność</a></h3>
<p><a href="https://w3c.github.io/trusted-types/dist/spec/" rel="noreferrer noopener">Specyfikacja Trusted Types API</a> jest już na dość zaawansowanym etapie standaryzacji. Choć wciąż może się zmieniać,  nie oczekiwałbym jakichś wielkich, wywrotowych zmian. Jak już jakieś nastąpią, to raczej kosmetyczne. Natomiast <a href="https://caniuse.com/trusted-types" rel="noreferrer noopener">wsparcie</a> jest już od dłuższego czasu zarówno w Chrome, jak i w Safari. W Firefoksie wciąż jest eksperymentalne za flagą.</p>
<p>Myślę, że dopóki wsparcie nie będzie we wszystkich najważniejszych przeglądarkach, najlepiej się ograniczyć do tworzenia domyślnej polityki i wymuszać ją przy pomocy dyrektywy <code>require-trusted-types-for</code>. Dzięki temu całość będzie jak najbardziej transparentna dla przeglądarek, które wciąż nie wspierają tego API. Natomiast cała reszta dostanie lepsze zabezpieczenie przed atakami XSS.</p>
<p>I w końcu będzie można zacząć&nbsp;ufać własności <code>#innerHTML</code>!</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Dezynfekcja</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/dezynfekcja" rel="alternate" type="text/html"/>
			<published>2025-12-16T23:00:00.000Z</published>
			<updated>2025-12-16T23:00:00.000Z</updated>
			<id>https://blog.comandeer.pl/dezynfekcja</id>
			
				<summary><![CDATA[Rzut oka na HTML Sanitizer API – kolejną broń w arsenale przeciwko atakom XSS.]]></summary>
			
			<content type="html"><![CDATA[<p>Internet nie jest bezpiecznym miejscem. Zagrożenia czyhają na każdym rogu. Jednym z nich są ataki XSS, które wymierzone są równocześnie w stronę&nbsp;WWW, jak i przeglądarkę. Na szczęście powstało nowe API, mające pomóc w ochronie przed nimi!</p>
<h2 id="xss--czyli-co-zagraża-aplikacji-www"><a class="header-anchor" href="https://blog.comandeer.pl/dezynfekcja#xss--czyli-co-zagraża-aplikacji-www">XSS – czyli co zagraża aplikacji WWW?</a></h2>
<p>Zacznijmy od tego, czym w ogóle jest atak <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/XSS" rel="noreferrer noopener">Cross-Site Scripting (XSS)</a>. W największym skrócie: to atak polegający na wstrzyknięciu złośliwego kodu na stronę WWW. Ten kod następnie zostanie wykonany przez przeglądarkę osoby odwiedzającej stronę. Taki kod może zrobić tak naprawdę wszystko, co potrafi JS w przeglądarce, np. pobranie ciasteczek i wysłanie ich do zewnętrznego serwera.</p>
<p>Wstrzyknięcia można dokonać na wiele różnych sposobów. Jednym z najczęstszych jest wykorzystanie formularzy na stronie, np. edytora postów na blogu czy księgi gości. Jeśli dostęp do takiego formularza mają też niezaufane osoby, wówczas jest spora szansa, że jedna z nich spróbuje zaatakować stronę. Choćby zostawiając poniższy wpis:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">img</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"x"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> onerror</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#6F42C1;--shiki-dark:#B392F0">alert</span><span style="color:#032F62;--shiki-dark:#9ECBFF">( 'Słaba ta strona!' )"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli w żaden sposób nie filtrujemy danych przychodzących, tego typu kod może trafić do bazy danych, a ostatecznie – do przeglądarek innych osób odwiedzających stronę. Każdej z tych osób pokaże się wówczas wyskakujące okienko z tekstem <q>Słaba ta strona!</q>. Dzieje się tak, ponieważ atrybut <code>[onerror]</code> dodaje do obrazka nasłuchiwacz dla <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/error_event" rel="noreferrer noopener">zdarzenia <code>error</code></a>. A to odpala się <a href="http://m.in" rel="noreferrer noopener">m.in</a> wówczas, gdy wczytywany obrazek nie istnieje. Jeśli nie mamy na stronie obrazka pod ścieżką <code>/x</code> (a mało kto ma), to zdarzenie się odpali i kod wykona się u niczego niepodejrzewających osób odwiedzających naszą stronę.</p>
<h2 id="istniejące-sposoby-ochrony"><a class="header-anchor" href="https://blog.comandeer.pl/dezynfekcja#istniejące-sposoby-ochrony">Istniejące sposoby ochrony</a></h2>
<p>Mimo że XSS to jeden z podstawowych typów ataków, niemal tak stary jak sam Internet, to wciąż jest niezwykle popularny. Wciąż mocno się&nbsp;trzyma na <a href="https://owasp.org/www-project-top-ten/" rel="noreferrer noopener">liście Top 10 OWASP</a> – raporcie opisującym dziesięć najpopularniejszych typów ataków w Internecie. Dlatego też wciąż potrzeba sensownych i sprawdzonych metod obrony przed nim. Dotąd trzeba było wykorzystywać w tym celu różne biblioteki. Jedną z najpopularniejszych jest <a href="https://github.com/cure53/DOMPurify" rel="noreferrer noopener">DOMPurify</a>, które działa nie tylko w przeglądarce, ale także w Node.js. Działanie tej biblioteki można zobaczyć w <a href="https://cure53.de/purify" rel="noreferrer noopener">oficjalnym demie</a>. Polega na usuwaniu z przekazanego ciągu tekstowego wszystkich niebezpiecznych fragmentów HTML-a. Jako przykład posłuży nam następujący kod:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">alert</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'script'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 1 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">img</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"x"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> alt</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> onerror</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#6F42C1;--shiki-dark:#B392F0">alert</span><span style="color:#032F62;--shiki-dark:#9ECBFF">('obrazek')"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 2 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> onmouseover</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#6F42C1;--shiki-dark:#B392F0">alert</span><span style="color:#032F62;--shiki-dark:#9ECBFF">('hover')"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Najedź mnie&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 3 --&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Powyższy kod HTML po odpaleniu w przeglądarce wyświetli trzy wyskakujące okienka:</p>
<ol>
<li>po wykonaniu skryptu z elementu <code>script</code>,</li>
<li>po błędzie wczytywania obrazka,</li>
<li>po najechaniu na tekst <q>Najedź mnie</q>.</li>
</ol>
<p>Po przepuszczeniu tego kodu przez DOMPurify dostaniemy:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">img</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"x"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> alt</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Najedź mnie&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wszystkie JS-owe elementy kodu zostały usunięte, tym samym zostawiając wyłącznie bezpieczny HTML.</p>
<h2 id="html-sanitizer-api--nowy-obrońca-sieci"><a class="header-anchor" href="https://blog.comandeer.pl/dezynfekcja#html-sanitizer-api--nowy-obrońca-sieci">HTML Sanitizer API – nowy obrońca Sieci</a></h2>
<p>W końcu doczekaliśmy się API chroniącego przed XSS-em, które jest wbudowane w przeglądarkę – <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API" rel="noreferrer noopener">HTML Sanitizer API (API Dezynfekujące HTML-a)</a>. Składają się na niego dwa główne elementy:</p>
<ol>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML" rel="noreferrer noopener">metoda <code>#setHTML()</code></a>, dołączona do elementów HTML i <a href="https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot" rel="noreferrer noopener">korzeni Shadow DOM</a>,</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/parseHTML_static" rel="noreferrer noopener">statyczna metoda <code>Document.parseHTML()</code></a>.</li>
</ol>
<h3 id="bezpieczny-zaułek"><a class="header-anchor" href="https://blog.comandeer.pl/dezynfekcja#bezpieczny-zaułek">Bezpieczny zaułek</a></h3>
<p>Metoda <code>#setHTML()</code> jest odpowiednikiem <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML" rel="noreferrer noopener">własności <code>#innerHTML</code></a>. Główna różnica między nimi polega na tym, że nowa metoda wyrzuca z przekazanego jej kodu HTML wszystkie niebezpieczne elementy:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">document.body.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">setHTML</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`&lt;script&gt;alert( 'script' );&lt;/script&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">&lt;img src="x" alt="" onerror="alert('obrazek')"&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">&lt;span onmouseover="alert('hover')"&gt;Najedź mnie&lt;/span&gt;`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wykonanie tego kodu sprawi, że zawartość strony zostanie podmieniona na następującą:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Najedź mnie&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jak widać, natywne API jest nawet dokładniejsze niż domyślne ustawienia biblioteki DOMPurify. Zniknął bowiem cały element <code>img</code>. Gdy przyjrzymy się <a href="https://wicg.github.io/sanitizer-api/#sanitization" rel="noreferrer noopener">algorytmowi dezynfekcji HTML-a</a> w specyfikacji, zauważymy, że jednym z kroków jest usuwanie elementów, które nie występują w konfiguracji dezynfekcji. Jeśli nie podamy własnego obiektu z opcjami, wówczas zostanie użyty <a href="https://wicg.github.io/sanitizer-api/#built-in-safe-default-configuration" rel="noreferrer noopener">domyślny</a> – a ten nie zawiera elementu <code>img</code>. Stąd nie ma go też w wynikowym HTML-u. Teoretycznie, żeby zachować obrazek, jako drugi argument metody <code>#setHTML()</code> trzeba podać <a href="https://developer.mozilla.org/en-US/docs/Web/API/SanitizerConfig" rel="noreferrer noopener">obiekt konfiguracyjny</a> – ale na ten moment nie działa to w żadnej przeglądarce.</p>
<p>Z kolei metoda statyczna <code>Document.parseHTML()</code> jest odpowiednikiem <a href="https://developer.mozilla.org/en-US/docs/Web/API/DOMParser" rel="noreferrer noopener">wbudowanego parsera HTML</a>. Metoda parsuje kod HTML, czyści go ze wszystkich niebezpiecznych fragmentów, a następnie tworzy nowy <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document" rel="noreferrer noopener">obiekt <code>Document</code></a> na jego podstawie:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( Document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">parseHTML</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`&lt;!DOCTYPE html&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    &lt;html lang="pl"&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        &lt;head&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            &lt;title&gt;Testowy dokument&lt;/title&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        &lt;/head&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        &lt;body&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            &lt;script&gt;alert( 'script' );&lt;/script&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            &lt;h1&gt;Hello, world!&lt;/h1&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        &lt;/body&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    &lt;/html&gt;`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Ten kod wyświetli w konsoli następujący dokument:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;!</span><span style="color:#22863A;--shiki-dark:#85E89D">DOCTYPE</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"pl"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">title</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Testowy dokument&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">title</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Hello, world!&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jak widać, dokument jest praktycznie identyczny jak ten, który przekazaliśmy metodzie <code>Document.parseHTML()</code>. Jedyną&nbsp;różnicą jest brak elementu <code>script</code>.</p>
<h3 id="niebezpieczna-manipulacja"><a class="header-anchor" href="https://blog.comandeer.pl/dezynfekcja#niebezpieczna-manipulacja">Niebezpieczna manipulacja</a></h3>
<p>Warto wspomnieć, że zarówno <code>#setHTML()</code>, jak i <code>Document.parseHTML()</code>, mają swoje “niebezpieczne” odpowiedniki – <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTMLUnsafe" rel="noreferrer noopener"><code>#setHTMLUnsafe()</code></a> oraz <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/parseHTMLUnsafe_static" rel="noreferrer noopener"><code>Document.parseHTMLUnsafe()</code></a>. Działają tak samo, ale nie usuwają niczego.</p>
<p>Tym samym, jeśli użyjemy <code>#setHTMLUnsafe()</code> na naszym testowym kodzie HTML:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">document.body.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">setHTMLUnsafe</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`&lt;script&gt;alert( 'script' );&lt;/script&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">&lt;img src="x" alt="" onerror="alert('obrazek')"&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">&lt;span onmouseover="alert('hover')"&gt;Najedź mnie&lt;/span&gt;`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>do zawartości strony trafi dokładnie ten kod HTML, bez żadnego czyszczenia:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">alert</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'script'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">img</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> src</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"x"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> alt</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> onerror</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#6F42C1;--shiki-dark:#B392F0">alert</span><span style="color:#032F62;--shiki-dark:#9ECBFF">('obrazek')"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> onmouseover</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"</span><span style="color:#6F42C1;--shiki-dark:#B392F0">alert</span><span style="color:#032F62;--shiki-dark:#9ECBFF">('hover')"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Najedź mnie&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">span</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Podobnie stanie się, jeśli podmienimy <code>Document.parseHTML()</code> na <code>Document.parseHTMLUnsafe()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( Document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">parseHTMLUnsafe</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`&lt;!DOCTYPE html&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    &lt;html lang="pl"&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        &lt;head&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            &lt;title&gt;Testowy dokument&lt;/title&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        &lt;/head&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        &lt;body&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            &lt;script&gt;alert( 'script' );&lt;/script&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">            &lt;h1&gt;Hello, world!&lt;/h1&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">        &lt;/body&gt;</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">    &lt;/html&gt;`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Ten kod stworzy dokładnie taki dokument HTML:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;!</span><span style="color:#22863A;--shiki-dark:#85E89D">DOCTYPE</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> lang</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"pl"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">title</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Testowy dokument&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">title</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">head</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span><span style="color:#6F42C1;--shiki-dark:#B392F0">alert</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'script'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">script</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Hello, world!&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">h1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">body</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">html</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><h3 id="kompatybilność"><a class="header-anchor" href="https://blog.comandeer.pl/dezynfekcja#kompatybilność">Kompatybilność</a></h3>
<p>Gdzie zatem można użyć nowego API? Otóż… nigdzie. Pojawić się powinno w <a href="https://caniuse.com/mdn-api_element_sethtml" rel="noreferrer noopener">Firefoksie 147</a>, którego wydanie planowane jest na 13 stycznia 2026 roku. W Chrome dostępne jest jako eksperymentalny ficzer, ukryty za <a href="https://developer.chrome.com/docs/web-platform/chrome-flags" rel="noreferrer noopener">flagą</a>. Na ten moment <a href="https://chromestatus.com/feature/5814067399491584" rel="noreferrer noopener">nie wiadomo</a>, kiedy trafi do Chrome’a jako stabilny ficzer. Na pewno nie pomaga temu fakt, że <a href="https://wicg.github.io/sanitizer-api/" rel="noreferrer noopener">specyfikacja</a> wciąż się zmienia i nie wyszła jeszcze z grupy inkubującej nowe standardy.</p>
<p>Zatem wygląda na to, że w najbliższej przyszłości rozwiązania pokroju DOMPurify wciąż będą jedynymi rozwiązaniami pozwalającymi dezynfekować kod HTML. Jednak przeglądarki mają w rękawie jeszcze jednego asa… Ale to już opowieść na inną okazję!</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Zagięty róg książki</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/zagiety-rog-ksiazki" rel="alternate" type="text/html"/>
			<published>2025-12-12T23:00:00.000Z</published>
			<updated>2025-12-12T23:00:00.000Z</updated>
			<id>https://blog.comandeer.pl/zagiety-rog-ksiazki</id>
			
				<summary><![CDATA[Czasami trzeba zrobić coś nietypowego z aktualnie odwiedzaną stroną WWW, ale pisanie rozszerzenia przeglądarki jest zbyt skomplikowane. Wówczas pozostają skryptozakładki!]]></summary>
			
			<content type="html"><![CDATA[<p>Czasami trzeba zrobić coś <em>nietypowego</em> z aktualnie odwiedzaną stroną. Ot, choćby wygenerować spis treści dla <a href="https://blog.comandeer.pl/xslt-jeszcze-zywa-skamielina-sieci">mojego wpisu o XSLT</a>. Jak to jednak zrobić jako osoba odwiedzająca stronę?</p>
<h2 id="czemu-nie-rozszerzenie-przeglądarki"><a class="header-anchor" href="https://blog.comandeer.pl/zagiety-rog-ksiazki#czemu-nie-rozszerzenie-przeglądarki">Czemu nie rozszerzenie przeglądarki?</a></h2>
<p>Pierwszą myślą zapewne są <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions" rel="noreferrer noopener">rozszerzenia przeglądarki</a>. Dzięki nim można dodawać do przeglądarki przeróżne nowe funkcje, poczynając od <a href="https://chromewebstore.google.com/detail/ublock-origin-lite/ddkjiahejlhfcafbddmgiahcphecmpfh?hl=en" rel="noreferrer noopener">blokowania reklam</a>, a kończąc na <a href="https://github.com/refined-github/refined-github" rel="noreferrer noopener">ulepszaniu interfejsu GitHuba</a>. Nie byłoby więc żadnym problemem dla rozszerzenia, żeby wygenerować spis treści na stronie.</p>
<p>Haczyk polega na tym, że napisanie rozszerzenia nie jest aż takie proste. Mimo że na przestrzeni lat zaszły zmiany, które to mocno ułatwiły, to wciąż jest to nieopłacalna ilość pracy, jeśli chcemy zrobić coś naprawdę małego. Rozszerzenia tworzy się przy pomocy <a href="https://developer.chrome.com/docs/extensions/reference/" rel="noreferrer noopener">specjalnego API</a>. Oczywiście, istnieją osobne jego wersje dla przeglądarek opartych o Chromium, Firefoksa oraz Safari. W praktyce rozszerzenie napisane dla Chrome’a da się <a href="https://extensionworkshop.com/documentation/develop/porting-a-google-chrome-extension/" rel="noreferrer noopener">łatwo dostosować pod Firefoksa</a>. Safari to osobna historia i <a href="https://developer.apple.com/documentation/safariservices/packaging-a-web-extension-for-safari" rel="noreferrer noopener">wymaga odpowiednich narzędzi</a>. Wciąż jednak można uznać proces tworzenia rozszerzenia pod wszystkie najważniejsze przeglądarki za względnie prosty. A doszliśmy do tego dzięki różnym <a href="https://github.com/w3c/webextensions" rel="noreferrer noopener">inicjatywom standaryzującym</a>.</p>
<p>Każde rozszerzenie <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension" rel="noreferrer noopener">składa się z kilku części</a>: <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension#manifest.json" rel="noreferrer noopener">manifestu (metadanych)</a>, <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Background_scripts" rel="noreferrer noopener">skryptów działających w tle</a>, <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts" rel="noreferrer noopener">skryptów działających na aktualnie odwiedzanej stronie</a> oraz <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Anatomy_of_a_WebExtension#sidebars_popups_and_options_pages" rel="noreferrer noopener">UI samego rozszerzenia</a> (np. okienka z opcjami). Jednak samo stworzenie rozszerzenia to dopiero połowa zabawy. Żeby było dostępne dla innych, trzeba je opublikować w sklepie z roszerzeniami danej przeglądarki. A każdy z nich ma inne wymagania i inny proces. Innymi słowy: dla małych rzeczy jest to gra niewarta świeczki.</p>
<h2 id="skryptozakładki"><a class="header-anchor" href="https://blog.comandeer.pl/zagiety-rog-ksiazki#skryptozakładki">Skryptozakładki</a></h2>
<p>Na szczęście istnieje pewien prosty sposób, dzięki któremu da się imitować działanie rozszerzeń: <a href="https://en.wikipedia.org/wiki/Bookmarklet" rel="noreferrer noopener"><i lang="en">bookmarklets</i> (skryptozakładki)</a>. To nic innego, jak zakładki w przeglądarce, które, zamiast odsyłać do strony WWW, wykonują jakąś akcję. Związane jest to ze specjalnym <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes" rel="noreferrer noopener">schematem URL-i</a>: <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/javascript" rel="noreferrer noopener"><code>javascript</code></a>. Jak sama jego nazwa sugeruje, pozwala on na stworzenie URL-a, który wykona kod JS. Przykładowy URL może wyglądać tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>javascript:alert(1)</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Jeśli ten kod wkleimy lub wpiszemy do paska adresu w przeglądarce i naciśniemy <kbd>Enter</kbd>, naszym oczom okaże się wyskakujący komunikat o treści <q>1</q>. Ten sam efekt można uzyskać, tworząc <a href="javascript:alert(1)">specjalny link</a>. Taki link można następnie przeciągnąć na pasek zakładek przeglądarki i zapisać sobie skryptozakładkę “na później”.</p>
<p>Do skryptozakładek można włożyć praktycznie dowolny kod JS. Dzięki temu można zrobić np. <a href="https://sa11y.netlify.app/bookmarklet/" rel="noreferrer noopener">narzędzie do sprawdzania dostępności</a>. Trzeba jedynie pamiętać, żeby taki kod był zgodny ze skłądnią URL-i, np. białe znaki powinny być <a href="https://developer.mozilla.org/en-US/docs/Glossary/Percent-encoding" rel="noreferrer noopener">odpowiednio zakodowane</a>. W przeglądarce służą do tego <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent" rel="noreferrer noopener">funkcja <code>encodeURIComponent()</code></a> oraz <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI" rel="noreferrer noopener">funkcja <code>encodeURI()</code></a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'javascript:'</span><span style="color:#D73A49;--shiki-dark:#F97583"> +</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> encodeURIComponent</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`alert( 'Hello, world!' );`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">encodeURI</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`javascript:alert( 'Hello, world!' );`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W przypadku <code>encodeURIComponent()</code> kodowaniu poddajemy tylko część URL-a po schemacie (1). Natomiast w przypadku <code>encodeURI()</code> – cały URL (2). Warto też zauważyć, że <code>encodeURIComponent()</code> koduje więcej znaków.</p>
<p>Skryptozakładki mają jednak jeden, bardzo spory haczyk: są traktowane jak zwyczajne skrypty JS. Zatem podlegają zasadom <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP" rel="noreferrer noopener"><i lang="en">Content Security Policy</i></a>. Jeśli dana strona nie zezwala na skrypty spoza konkretnego <a href="https://developer.mozilla.org/en-US/docs/Glossary/Origin" rel="noreferrer noopener">originu</a>, skryptozakładka nie zadziała. Przeglądarki nakładają też limity na długość&nbsp;URL-i i dłuższe skrypty po prostu się nie zmieszczą&nbsp;w skryptozakładkach. <a href="https://stackoverflow.com/a/417184" rel="noreferrer noopener">Sensownym limitem wydaje się długość&nbsp;ok. 8000 znaków</a>.</p>
<h2 id="skryptozakładka-generująca-spis-treści"><a class="header-anchor" href="https://blog.comandeer.pl/zagiety-rog-ksiazki#skryptozakładka-generująca-spis-treści">Skryptozakładka generująca spis treści</a></h2>
<h3 id="kod"><a class="header-anchor" href="https://blog.comandeer.pl/zagiety-rog-ksiazki#kod">Kod</a></h3>
<p>Napiszmy zatem kod naszej skryptozakładki generującej spis treści. Będzie ona robić kilka rzeczy:</p>
<ol>
<li>wyszukiwać wszystkie nagłówki,</li>
<li>tworzyć na ich podstawie listę z linkami,</li>
<li>wstawiać tę listę jako przypięty element z lewej strony.</li>
</ol>
<p>Z racji tego, że to skryptozakładka, całość otoczona będzie w <a href="https://developer.mozilla.org/en-US/docs/Glossary/IIFE" rel="noreferrer noopener">IIFE</a>. Dzięki temu odpali się w momencie kliknięcia w link:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}() );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Dodajmy teraz logikę odpowiedzialną za wyszukiwanie nagłówków i tworzenie z nich listy z linkami:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> headings</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">querySelectorAll</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'main :where(h1, h2, h3, h4, h5, h6)'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentLevel </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nav</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createList</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( currentLevel ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentList </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> nav; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> heading</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> headings ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		setLevel</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">getHeadingLevel</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( heading ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> listItem</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createListItem</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( heading ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		currentList.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( listItem ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	document.body.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">createNavContainer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( nav ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}() );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku wyszukujemy wszystkie nagłówki, które są wewnątrz elementu <code>main</code> (1). Dzięki temu odsiejemy nieinteresujące nas nagłówki, jak np. te od modala z ciasteczkami. Warto zauważyć jak <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:where" rel="noreferrer noopener">pseudoklasa <code>:where()</code></a> pomaga nam uzyskać prostszy selektor. Potem ustawiamy aktualny poziom zagłębienia nagłówków (2), tworzymy listę nagłówków danego poziomu (3) oraz ustawiamy ją jako aktualną listę (4). Następnie iterujemy po nagłówkach (5) i dla każdego ustawiamy poziom zagłębienia (6). Następnie tworzymy nowy element listy na podstawie nagłówka (7) i dodajemy go do aktualnej listy (8). Na samym końcu tworzymy kontener z nawigacją i wstawiamy go na stronę (9).</p>
<p>Na pierwszy rzut oka ta logika może wydawać się dziwna, ale chcemy uzyskać listę, w której poszczególne nagłówki będą miały odpowiedni poziom zagłębienia:</p>
<ul>
<li>Główny nagłówek artykułu
<ul>
<li>Podsekcja
<ul>
<li>Podpodsekcja</li>
</ul>
</li>
<li>Kolejna podsekcja</li>
</ul>
</li>
</ul>
<p>Żeby to uzyskać, nasz skrypt:</p>
<ol>
<li>Trzyma informację o tym, na jakim poziomie zagłębienia jest.</li>
<li>Jeśli kolejny dodawany nagłówek jest na wyższym poziomie zagłębienia:
<ol>
<li>Dodaje nową zagnieżdżoną listę.</li>
<li>Dodaje do tej nowej listy link do nagłówka.</li>
</ol>
</li>
<li>Jeśli kolejny dodawany nagłówek jest na niższym poziomie zagłębienia:
<ol>
<li>Wyszukuje najbliższą listę, która ma ten poziom zagłębienia.</li>
<li>Dodaje do tej listy link do nagłówka.</li>
</ol>
</li>
<li>Jeśli kolejny dodawany nagłówek jest na tym samym poziomie zagłębienia:
<ol>
<li>Dodaje do obecnej listy link do nagłówka.</li>
</ol>
</li>
</ol>
<p>Ten algorytm jest implementowany przez funkcję <code>setLevel()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> setLevel</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">level</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> lastLevel</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentLevel; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	currentLevel </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> level; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( lastLevel </span><span style="color:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> level ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> newList</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createList</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( level ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		currentList.lastElementChild.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( newList ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		currentList </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> newList; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} </span><span style="color:#D73A49;--shiki-dark:#F97583">else</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		currentList </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentList.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">closest</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`[data-level="${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> level</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }"]`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku zapisujemy obecny poziom zagłębienia do zmiennej <code>lastLevel</code> (1). Dzięki temu możemy nadpisać aktualny poziom tym przekazanym do funkcji (2). Następnie sprawdzamy, czy nowy poziom jest większy od poprzedniego (3). Jeśli tak, tworzymy nową listę z nowym poziomem zagłębienia (4) i dodajemy ją do tej obecnej (5). Następnie ustawiamy obecną&nbsp;listę na tę nowo utworzoną (6). Jeśli nie (7), znajdujemy najbliższą listę z nowym poziomem zagłębienia (8). <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/closest" rel="noreferrer noopener">Metoda <code>#closest()</code></a> tak naprawdę pokrywa punkty 3. i 4. algorytmu, ponieważ w przypadku, gdy nowy poziom zagłębienia jest taki sam jak stary, <code>#closest()</code> też zwróci poprawną listę.</p>
<p>Samym tworzeniem listy zajmuje się&nbsp;funkcja <code>createList()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createList</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">level</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> list</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'ul'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	list.style.listStyleType </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'none'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	list.dataset.level </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> level; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> list; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Przekazujemy jej poziom zagłębienia listy (1), który dodajemy jako atrybut <code>[data-level]</code> do samej listy (2). Oprócz tego, funkcja tworzy listę&nbsp;(3), usuwa z niej styl punktora (4), a na końcu zwraca stworzoną listę (5).</p>
<p>Z kolei element listy z linkiem do nagłówka jest tworzony przy pomocy funkcji <code>createListItem()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createListItem</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">heading</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> listItem</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'li'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	listItem.innerHTML </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> heading.innerHTML;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> listItem;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zawartość elementu listy (1) to po prostu prosta kopia zawartości nagłówka. Z racji tego, że nagłówki na blogu już są podlinkowane, taki zabieg w pełni wystarcza do stworzenia działającego spisu treści.</p>
<p>Dla kompletności dorzucam jeszcze kod funkcji <code>getHeadingLevel()</code> oraz <code>createNavContainer()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getHeadingLevel</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">heading</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( heading.tagName.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'H'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createNavContainer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">nav</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> container</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'div'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	container.style.position </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'fixed'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	container.style.insetInlineStart </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	container.style.insetBlockStart </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	container.style.border </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '2px var(--secondary) solid'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	container.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( nav );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> container;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Funkcja <code>getHeadingLevel()</code> wyciąga poziom nagłówka z <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/tagName" rel="noreferrer noopener">nazwy jego elementu HTML</a> (1). Wycina z niej literę&nbsp;<code>H</code>, a resztę konwertuje z ciągu tekstowego na liczbę. Z kolei funkcja <code>createNavContainer()</code> tworzy element <code>div</code> (1), a następnie nadaje mu odpowiednie style, by był przyczepiony (2) w lewym (3), górnym (4) rogu strony.</p>
<p>Całość kodu prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> headings</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">querySelectorAll</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'main :where(h1, h2, h3, h4, h5, h6)'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentLevel </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> nav</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createList</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( currentLevel );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentList </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> nav;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> heading</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> headings ) {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		setLevel</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">getHeadingLevel</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( heading ) );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> listItem</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createListItem</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( heading );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		currentList.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( listItem );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	document.body.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">createNavContainer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( nav ) );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createList</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">level</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> list</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'ul'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		list.style.listStyleType </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'none'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		list.dataset.level </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> level;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> list;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> setLevel</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">level</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> lastLevel</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentLevel;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		currentLevel </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> level;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( lastLevel </span><span style="color:#D73A49;--shiki-dark:#F97583">&lt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> level ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> newList</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createList</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( level );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			currentList.lastElementChild.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( newList );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			currentList </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> newList;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		} </span><span style="color:#D73A49;--shiki-dark:#F97583">else</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			currentList </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> currentList.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">closest</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`[data-level="${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> level</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }"]`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getHeadingLevel</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">heading</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Number</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( heading.tagName.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'H'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createListItem</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">heading</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> listItem</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'li'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		listItem.innerHTML </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> heading.innerHTML;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> listItem;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createNavContainer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">nav</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> container</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">createElement</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'div'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		container.style.position </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'fixed'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		container.style.insetInlineStart </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		container.style.insetBlockStart </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 0</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		container.style.border </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '2px var(--secondary) solid'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		container.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">append</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( nav );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> container;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}() );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><h3 id="tworzenie-skryptozakładki"><a class="header-anchor" href="https://blog.comandeer.pl/zagiety-rog-ksiazki#tworzenie-skryptozakładki">Tworzenie skryptozakładki</a></h3>
<p>Tak naprawdę samo stworzenie skryptozakładki sprowadza się do przepuszczenia powyższego kodu JS przez funkcję <code>encodeURIComponent()</code>, a następnie zrobienie z tego URL-a. Ale że to nudne, przygotowałem proste narzędzie do konwersji:</p>
<div><embed- src="https://codepen.io/Comandeer/pen/MYyRgBB"><p class="embed-fallback"><a href="https://codepen.io/Comandeer/pen/MYyRgBB" rel="noreferrer noopener">Przejdź bezpośrednio do osadzonej treści na CodePenie.</a></p></embed-></div>
<p>Generator składa się z dwóch zasadniczych części: formularza w HTML-u oraz JS-owego kodu jego obsługi. Sam formularz jest prosty:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">HTML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">form</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"generator"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">label</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> for</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"code"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;JS code&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">label</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">textarea</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"code"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"code"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">textarea</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 1 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">button</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Generate&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">button</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">output</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"bookmarklet"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">output</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 2 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;</span><span style="color:#22863A;--shiki-dark:#85E89D">a</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> href</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> id</span><span style="color:#24292E;--shiki-dark:#E1E4E8">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF">"link"</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> hidden</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;Drag me to your bookmark bar&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">a</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">p</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt; </span><span style="color:#6A737D;--shiki-dark:#6A737D">&lt;!-- 3 --&gt;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">&lt;/</span><span style="color:#22863A;--shiki-dark:#85E89D">form</span><span style="color:#24292E;--shiki-dark:#E1E4E8">&gt;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Istotne&nbsp;są w nim tak naprawdę trzy elementy:</p>
<ol>
<li>pole tekstowe, w którym trzeba wprowadzić kod do przetworzenia,</li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/output" rel="noreferrer noopener">element <code>output</code></a>, w którym będzie wyświetlany gotowy URL,</li>
<li>link, dzięki któremu będzie można łatwo zainstalować skryptozakładkę w przeglądarce.</li>
</ol>
<p>Natomiast kod obsługujący ten formularz prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> form</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">querySelector</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'#generator'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> output</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">querySelector</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'#bookmarklet'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> link</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> document.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">querySelector</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'#link'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">form.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">addEventListener</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'submit'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">evt</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	evt.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">preventDefault</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> formData</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> FormData</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( evt.target ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rawCode</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> formData.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'code'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( rawCode.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		link.hidden </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		output.value </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ''</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> optimizedCode</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> rawCode.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replaceAll</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#005CC5;--shiki-dark:#79B8FF">[\t\n]</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#D73A49;--shiki-dark:#F97583">gu</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> bookmarklet</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `javascript:${</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> encodeURIComponent</span><span style="color:#032F62;--shiki-dark:#9ECBFF">( </span><span style="color:#24292E;--shiki-dark:#E1E4E8">optimizedCode</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ) </span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 10</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	output.value </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> bookmarklet; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 11</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	link.href </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> bookmarklet; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 12</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	link.hidden </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 13</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p><a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener" rel="noreferrer noopener">Nasłuchujemy</a> na <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event" rel="noreferrer noopener">zdarzenie <code>submit</code></a> formularza (1). <a href="https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault" rel="noreferrer noopener">Blokujemy domyślną akcję</a> (2), a następnie <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData" rel="noreferrer noopener">wyciągamy dane z formularza</a> (3). Sprawdzamy, czy zawartość&nbsp;pola tekstowego (4) jest pusta (5). Jeśli tak, ukrywamy linka (6), resetujemy zawartość elementu <code>output</code> (7) i wychodzimy z funkcji (8). W przeciwnym wypadku, optymalizujemy zawartość pola tekstowego poprzez usunięcie tabulatorów i znaków nowej linii (9). Dzięki temu pozbywamy się sporej części białych znaków i tym samym – zmniejszamy rozmiar wynikowego URL-a. Na sam koniec tworzymy URL-a (10) i wsadzamy go do elementu <code>output</code> (11) oraz do linku (12), który przy okazji pokazujemy (13).</p>
<h3 id="gotowa-skryptozakładka"><a class="header-anchor" href="https://blog.comandeer.pl/zagiety-rog-ksiazki#gotowa-skryptozakładka">Gotowa skryptozakładka</a></h3>
<p>Jeśli do powyższego generatora wkleimy nasz kod generujący spis treści, otrzymamy następującego URL-a:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>javascript:(%20function()%20%7Bconst%20headings%20%3D%20document.querySelectorAll(%20'main%20%3Awhere(h1%2C%20h2%2C%20h3%2C%20h4%2C%20h5%2C%20h6)'%20)%3Blet%20currentLevel%20%3D%201%3Bconst%20nav%20%3D%20createList(%20currentLevel%20)%3Blet%20currentList%20%3D%20nav%3Bfor%20(%20const%20heading%20of%20headings%20)%20%7BsetLevel(%20getHeadingLevel(%20heading%20)%20)%3Bconst%20listItem%20%3D%20createListItem(%20heading%20)%3BcurrentList.append(%20listItem%20)%3B%7Ddocument.body.append(%20createNavContainer(%20nav%20)%20)%3Bfunction%20createList(%20level%20)%20%7Bconst%20list%20%3D%20document.createElement(%20'ul'%20)%3Blist.style.listStyleType%20%3D%20'none'%3Blist.dataset.level%20%3D%20level%3Breturn%20list%3B%7Dfunction%20setLevel(%20level%20)%20%7Bconst%20lastLevel%20%3D%20currentLevel%3BcurrentLevel%20%3D%20level%3Bif%20(%20lastLevel%20%3C%20level%20)%20%7Bconst%20newList%20%3D%20createList(%20level%20)%3BcurrentList.lastElementChild.append(%20newList%20)%3BcurrentList%20%3D%20newList%3B%7D%20else%20%7BcurrentList%20%3D%20currentList.closest(%20%60%5Bdata-level%3D%22%24%7B%20level%20%7D%22%5D%60%20)%3B%7D%7Dfunction%20getHeadingLevel(%20heading%20)%20%7Breturn%20Number(%20heading.tagName.replace(%20'H'%2C%20''%20)%20)%3B%7Dfunction%20createListItem(%20heading%20)%20%7Bconst%20listItem%20%3D%20document.createElement(%20'li'%20)%3BlistItem.innerHTML%20%3D%20heading.innerHTML%3Breturn%20listItem%3B%7Dfunction%20createNavContainer(%20nav%20)%20%7Bconst%20container%20%3D%20document.createElement(%20'div'%20)%3Bcontainer.style.position%20%3D%20'fixed'%3Bcontainer.style.insetInlineStart%20%3D%200%3Bcontainer.style.insetBlockStart%20%3D%200%3Bcontainer.style.border%20%3D%20'2px%20var(--secondary)%20solid'%3Bcontainer.append(%20nav%20)%3Breturn%20container%3B%7D%7D()%20)%3B</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Teraz wystarczy go zapisać jako zakładkę (np. przeciągając <a href="javascript:(%20function()%20%7Bconst%20headings%20%3D%20document.querySelectorAll(%20'main%20%3Awhere(h1%2C%20h2%2C%20h3%2C%20h4%2C%20h5%2C%20h6)'%20)%3Blet%20currentLevel%20%3D%201%3Bconst%20nav%20%3D%20createList(%20currentLevel%20)%3Blet%20currentList%20%3D%20nav%3Bfor%20(%20const%20heading%20of%20headings%20)%20%7BsetLevel(%20getHeadingLevel(%20heading%20)%20)%3Bconst%20listItem%20%3D%20createListItem(%20heading%20)%3BcurrentList.append(%20listItem%20)%3B%7Ddocument.body.append(%20createNavContainer(%20nav%20)%20)%3Bfunction%20createList(%20level%20)%20%7Bconst%20list%20%3D%20document.createElement(%20'ul'%20)%3Blist.style.listStyleType%20%3D%20'none'%3Blist.dataset.level%20%3D%20level%3Breturn%20list%3B%7Dfunction%20setLevel(%20level%20)%20%7Bconst%20lastLevel%20%3D%20currentLevel%3BcurrentLevel%20%3D%20level%3Bif%20(%20lastLevel%20%3C%20level%20)%20%7Bconst%20newList%20%3D%20createList(%20level%20)%3BcurrentList.lastElementChild.append(%20newList%20)%3BcurrentList%20%3D%20newList%3B%7D%20else%20%7BcurrentList%20%3D%20currentList.closest(%20%60%5Bdata-level%3D%22%24%7B%20level%20%7D%22%5D%60%20)%3B%7D%7Dfunction%20getHeadingLevel(%20heading%20)%20%7Breturn%20Number(%20heading.tagName.replace(%20'H'%2C%20''%20)%20)%3B%7Dfunction%20createListItem(%20heading%20)%20%7Bconst%20listItem%20%3D%20document.createElement(%20'li'%20)%3BlistItem.innerHTML%20%3D%20heading.innerHTML%3Breturn%20listItem%3B%7Dfunction%20createNavContainer(%20nav%20)%20%7Bconst%20container%20%3D%20document.createElement(%20'div'%20)%3Bcontainer.style.position%20%3D%20'fixed'%3Bcontainer.style.insetInlineStart%20%3D%200%3Bcontainer.style.insetBlockStart%20%3D%200%3Bcontainer.style.border%20%3D%20'2px%20var(--secondary)%20solid'%3Bcontainer.append(%20nav%20)%3Breturn%20container%3B%7D%7D()%20)%3B">ten pomocny link</a>). Kiedy już ją odpalimy na wybranym artykule, po lewej stronie powinien się pojawić&nbsp;spis treści:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/zagiety-rog-ksiazki/spis-tresci-1324w.avif"><picture><source type="image/avif" srcset="/assets/images/zagiety-rog-ksiazki/spis-tresci-440w.avif 440w, /assets/images/zagiety-rog-ksiazki/spis-tresci-880w.avif 880w, /assets/images/zagiety-rog-ksiazki/spis-tresci-1024w.avif 1024w, /assets/images/zagiety-rog-ksiazki/spis-tresci-1324w.avif 1324w" sizes="90vw"><source type="image/webp" srcset="/assets/images/zagiety-rog-ksiazki/spis-tresci-440w.webp 440w, /assets/images/zagiety-rog-ksiazki/spis-tresci-880w.webp 880w, /assets/images/zagiety-rog-ksiazki/spis-tresci-1024w.webp 1024w, /assets/images/zagiety-rog-ksiazki/spis-tresci-1324w.webp 1324w" sizes="90vw"><source type="image/png" srcset="/assets/images/zagiety-rog-ksiazki/spis-tresci-440w.png 440w, /assets/images/zagiety-rog-ksiazki/spis-tresci-880w.png 880w, /assets/images/zagiety-rog-ksiazki/spis-tresci-1024w.png 1024w, /assets/images/zagiety-rog-ksiazki/spis-tresci-1324w.png 1324w" sizes="90vw"><img src="/assets/images/zagiety-rog-ksiazki/spis-tresci-1324w.png" width="1324" height="331" style="aspect-ratio: 1324 / 331" alt="Fragment wpisu &quot;XSLT – (jeszcze) żywa skamielina Sieci&quot;; w lewym górnym rogu znajduje się&nbsp;ramka ze spisem treści wpisu z linkami do poszczególnych nagłówków." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Przykład działania na <a href="https://blog.comandeer.pl/xslt-jeszcze-zywa-skamielina-sieci">wpisie o XSLT</a></p></figcaption></figure>
<p>Tak oto w zamierzchłych czasach rozszerzało się możliwości przeglądarki!</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Wielka ucieczka</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/wielka-ucieczka" rel="alternate" type="text/html"/>
			<published>2025-12-11T23:00:00.000Z</published>
			<updated>2025-12-11T23:00:00.000Z</updated>
			<id>https://blog.comandeer.pl/wielka-ucieczka</id>
			
				<summary><![CDATA[Czasami Markdown i Liquid się gryzą i trzeba je jakoś pogodzić.]]></summary>
			
			<content type="html"><![CDATA[<p>W <a href="https://www.11ty.dev/" rel="noreferrer noopener">Eleventy</a> pliki <code>.md</code> to nie do końca <em>zwykły</em> <a href="https://commonmark.org/" rel="noreferrer noopener">Markdown</a>. Eleventy pozwala w nich bowiem mieszać Markdowna ze składnią <a href="https://liquidjs.com/index.html" rel="noreferrer noopener">Liquida</a>. To działa bardzo fajnie… do momentu, w którym nie dochodzi do <em>konfliktów</em>.</p>
<h2 id="poważne-kłótnie"><a class="header-anchor" href="https://blog.comandeer.pl/wielka-ucieczka#poważne-kłótnie">Poważne kłótnie</a></h2>
<p>Gdy pisałem artykuł o dygresjach, próbowałem pokazać, jak wygląda składnia nowego rozwiązania. Użyłem wówczas zapisu ze znakami ucieczki:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>{\% note %}Treść dygresji{\% endnote %}</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Wszystko po to, żeby Eleventy nie potraktował tego jako kodu w składni Liquida i nie wygenerował dygresji.</p>
<p>Myślałem, że w ten prymitywny sposób udało mi się ominąć problem. A potem pojawił się wpis o tworzeniu dyskusji przy pomocy GitHub Actions i okazało się, że tak nie do końca:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/wielka-ucieczka/blad-380w.avif"><picture><source type="image/avif" srcset="/assets/images/wielka-ucieczka/blad-380w.avif 380w" sizes="90vw"><source type="image/webp" srcset="/assets/images/wielka-ucieczka/blad-380w.webp 380w" sizes="90vw"><source type="image/png" srcset="/assets/images/wielka-ucieczka/blad-380w.png 380w" sizes="90vw"><img src="/assets/images/wielka-ucieczka/blad-380w.png" width="380" height="98" style="aspect-ratio: 380 / 98" alt="Błąd renderowania bloczka kodu: z wartości zmiennej &quot;GITHUB_TOKEN&quot;&nbsp;został jedynie znak &quot;$&quot;." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Przykład uciętej zmiennej środowiskowej</p></figcaption></figure>
<p>Ten fragment kodu miał wyglądać następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">YAML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">env</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  GITHUB_TOKEN</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">${{ secrets.GITHUB_TOKEN }}</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 17</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>GitHub Actions do wstawiania wartości zmiennych używa takiej samej składni co Liquid, czyli wąsów (<code>{{ }}</code>). Zatem Eleventy spróbowało podstawić wartość zmiennej z Liquida o nazwie <code>secrets.GITHUB_TOKEN</code> do treści. Takiej zmiennej jednak nie ma, więc ostatecznie zostało puste miejsce.</p>
<p>Stwierdziłem zatem, że coś trzeba z tym zrobić. W końcu musi być jakiś sposób, żeby ominąć problem nakładających się składni!</p>
<h2 id="rozwiązanie"><a class="header-anchor" href="https://blog.comandeer.pl/wielka-ucieczka#rozwiązanie">Rozwiązanie</a></h2>
<p>Zajrzałem więc do dokumentacji Liquida. Okazało się, że, faktycznie, jest coś, co pozwoli mi rozwiązać mój problem: <a href="https://liquidjs.com/tags/raw.html" rel="noreferrer noopener">tag <code>raw</code></a>. Jeśli otoczy się nim fragment treści, to nie będzie on parsowany przez Liquida. Tym samym – powinien przetrwać w niezmienionej formie i wylądować w wynikowym pliku HTML!</p>
<p>Co jeszcze ciekawsze, okazało się, że… już kiedyś ten tag zastosowałem. A dokładniej: we <a href="https://github.com/Comandeer/blog/blob/662e87d6ecd8fd683b95fc587aa4678cb939db45/src/_posts/javascript/2017-05-21-reduce-i-formatowanie-tekstu.md#L204" rel="noreferrer noopener">wpisie o formatowaniu tekstu przy pomocy metody <code>Array#reduce()</code></a>. Jakoś totalnie o tym zapomniałem. Niemniej, ponownie uzbrojony w tę wiedzę mogłem teraz poprawić problematyczne wpisy!</p>
<p>Tylko że… jestem leniwym i zapominalskim człowiekiem. Raz, że nie chce mi się wszędzie pisać tego tagu <code>raw</code>; dwa – nie jest mi on potrzebny na tyle często, żebym o nim pamiętał. Przy kolejnym tego typu problemie jeszcze raz bym musiał przechodzić cały proces przypominania sobie o tym rozwiązaniu. Wypadałoby to jakoś zautomatyzować!</p>
<h2 id="dobre-rozwiązanie"><a class="header-anchor" href="https://blog.comandeer.pl/wielka-ucieczka#dobre-rozwiązanie"><em>Dobre</em> rozwiązanie</a></h2>
<p>Na szczęście Eleventy udostępnia <a href="https://www.11ty.dev/docs/config-preprocessors/" rel="noreferrer noopener">preprocesory</a>. Pozwalają one zmodyfikować pliki wpisów, zanim trafią one do parserów Liquida i Markdowna. Jest więc to idealny moment, żeby zaaplikować tag <code>raw</code> w odpowiednich miejscach!</p>
<p>Tak wygląda mój preprocesor:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> codeBlockRegex</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#D73A49;--shiki-dark:#F97583">^</span><span style="color:#032F62;--shiki-dark:#DBEDFF">```(?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">.</span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#032F62;--shiki-dark:#DBEDFF">)</span><span style="color:#D73A49;--shiki-dark:#F97583">?</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#DBEDFF">(?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">.</span><span style="color:#D73A49;--shiki-dark:#F97583">|</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#DBEDFF">)</span><span style="color:#D73A49;--shiki-dark:#F97583">+?</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#DBEDFF">```</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#D73A49;--shiki-dark:#F97583">gmu</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createLiquidPreprocessor</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">eleventyConfig</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	eleventyConfig.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">addPreprocessor</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'liquidPreprocessor'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'md'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">_</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">content</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> content.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replaceAll</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( codeBlockRegex, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\%</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> raw %}$&amp;{</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\%</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> endraw %}'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><div class="note" role="note" aria-labelledby="note-60"><p class="note__label" id="note-60">Dygresja</p><div class="note__content"><p>Cóż, tag <code>raw</code> to ten jeden, w którym trzeba stosować znaki ucieczki – tego już się raczej nie da obejść…</p></div></div>
<p>Tworzymy funkcję <code>createLiquidPreprocessor()</code> (1), którą eksportujemy. Ta funkcja jako argument przyjmuje <a href="https://www.11ty.dev/docs/config/" rel="noreferrer noopener">obiekt konfiguracyjny Eleventy</a>. Wywołujemy jego metodę <code>#addPreprocessor()</code> (2) i przekazujemy mu nasz preprocesor. Podajemy jego nazwę (<code>liquidPreprocessor</code>) oraz rozszerzenie pliku, dla którego ma się odpalać (<code>md</code>). Sam preprocesor przekazujemy jako callback. Jego drugi argument to treść pliku z wpisem. Z callbacku zwracamy treść z podmienionymi wszystkimi blokami kodu na takie otoczone tagiem <code>raw</code> (3). Bloki kodu wykrywamy przy pomocy wyrażenia regularnego (4):</p>
<ol>
<li><code>^```</code> oznacza linię, która zaczyna się od potrójnego grawisa (`),</li>
<li><code>(?:.+)?</code> oznacza nazwę języka, która może składać się z dowolnej liczby dowolnych znaków (<code>.+</code>) oraz może wystąpić raz lub wcale (<code>?</code>),</li>
<li><code>\n</code> oznacza nową linię, następującą po grawisach i opcjonalnej nazwie języka,</li>
<li><code>(?:.|\n)+?</code> oznacza zawartość bloczka kodu, która może składać się z dowolnych znaków oraz nowych linii,</li>
<li><code>\n```\n</code> oznacza zakończenie bloczka, czyli potrójny grawis otoczony znakami nowej linii (inaczej mówiąc: linia, w której znajdują się wyłącznie trzy grawisy).</li>
</ol>
<p>Natomiast samą podmianę robimy przy pomocy wzorca <code>'{% raw %}$&amp;{\% endraw %}'</code>. Metoda <code>#replaceAll()</code> pozwala używać <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#specifying_a_string_as_the_replacement" rel="noreferrer noopener">specjalnych wyrażeń</a>, które dają dostęp do rzeczy znalezionych przez wyrażenie regularne. W naszym wypadku używamy wyrażenia <code>$&amp;</code> oznaczającego cały znaleziony ciąg.</p>
<p>Ostatnim etapem jest zaimportowanie funkcji <code>createLiquidPreprocessor()</code> w pliku <code>eleventyConfig.js</code> i wywołanie jej. Od teraz wszystkie bloczki kodu w plikach <code>.md</code> będą automatycznie otaczane przy pomocy tagów <code>raw</code>!</p>
<p>To rozwiązanie ma jedną wadę: nie pozwala używać podstawiania zmiennych Liquidowych wewnątrz bloczków kodu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> zmienna</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '{{ zmiennaZLiquida }}'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zamiast wartości zmiennej, powyższy kod wyświetli po prostu <code>{{ zmiennaZLiquida }}</code>. Niemniej od momentu powstania bloga ani razu tego nie potrzebowałem, więc… brzmi jak coś, co mogę odłożyć na później.</p>
<p>Tak oto niniejszy blog ulepszył się o kolejną, małą rzecz!</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Całkowicie swobodna dyskusja</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/calkowicie-swobodna-dyskusja" rel="alternate" type="text/html"/>
			<published>2025-12-07T23:10:00.000Z</published>
			<updated>2025-12-07T23:10:00.000Z</updated>
			<id>https://blog.comandeer.pl/calkowicie-swobodna-dyskusja</id>
			
				<summary><![CDATA[Druga część walki z komentarzami na blogu. Tym razem mierzymy się z komentarzami do nowych wpisów!]]></summary>
			
			<content type="html"><![CDATA[<p>Wczoraj opisałem <a href="https://blog.comandeer.pl/swobodna-dyskusja">częściowe rozwiązanie problemu z komentarzami</a>. Dzisiaj pora zająć&nbsp;się drugą częścią problemu i pozbyć&nbsp;się go raz na zawsze!</p>
<h2 id="co-udało-się-dotąd-osiągnąć"><a class="header-anchor" href="https://blog.comandeer.pl/calkowicie-swobodna-dyskusja#co-udało-się-dotąd-osiągnąć">Co udało się dotąd osiągnąć?</a></h2>
<p>Dla przypomnienia: <a href="https://github.com/Comandeer/blog/discussions/51" rel="noreferrer noopener">problem z komentarzami</a> polegał na tym, że nie istniały dyskusje dla wszystkich wpisów. Przez to osoby, które nie zaakceptowały ciasteczek, nie miały możliwości dodawania komentarzy.</p>
<p>Dlatego stworzyłem skrypt, który sprawdza, jakie dyskusje istnieją, a następnie tworzy wszystkie brakujące. W tym celu wykorzystałem <a href="https://docs.github.com/en/graphql" rel="noreferrer noopener">APi GitHuba</a> i <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens" rel="noreferrer noopener">personalny token dostępowy</a>. Niemniej to rozwiązanie naprawia komentarze tylko już&nbsp;w istniejących wpisach. Jeśli teraz dodałbym nowy, w nim problem znowu by się pojawił. Więc musiałbym regularnie odpalać swój naprawiający dyskusje skrypt… Albo przygotować rozwiązanie, które dodaje dyskusje dla każdego nowego wpisu.</p>
<h2 id="na-ratunek-github-actions"><a class="header-anchor" href="https://blog.comandeer.pl/calkowicie-swobodna-dyskusja#na-ratunek-github-actions">Na ratunek GitHub Actions!</a></h2>
<p>Z racji tego, że moim hobby jest spędzanie kilku godzin na próbie automatyzacji 3-minutowych czynności, postanowiłem zautomatyzować dodawanie dyskusji dla każdego nowego wpisu. Na całe szczęście GitHub udostępnia narzędzie, które to zdecydowanie ułatwia – <a href="https://github.com/features/actions" rel="noreferrer noopener">GitHub Actions</a>. Jest to rozwiązanie typu CI/CD – <a href="https://en.wikipedia.org/wiki/Continuous_integration" rel="noreferrer noopener"><i lang="en">Continuous Integration</i></a> (Ciągła Integracja)/<a href="https://en.wikipedia.org/wiki/Continuous_delivery" rel="noreferrer noopener"><i lang="en">Continuous Delivery</i></a> (Ciągłe Dostarczanie). Tego typu rozwiązania służą do monitoringu zmian w kodzie. Jeśli coś trafia do repozytorium, system CI/CD dba o to, by spełniało określone standardy. Odpali testy, wygeneruje dokumentację, a następnie może nawet automatycznie opublikować paczkę w npm-ie czy innym repozytorium pakietów. Słowem: automatyzuje te części procesu wypuszczania oprogramowania, które warto zautomatyzować.</p>
<p>GitHub Actions integrują się ze zdarzeniami, które mogą zajść&nbsp;w repozytorium na GitHubie, takimi jak <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#issues" rel="noreferrer noopener">dodanie nowego issue</a> czy <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#push" rel="noreferrer noopener">wypchnięcie zmian do konkretnego brancha</a>. Dzięki temu można np. sprawdzić, czy osoba zgłaszająca błąd dodała potrzebne informacje, albo wypuścić nową wersję pakietu npm za każdym razem, gdy nowy kod trafi do głównego brancha. Można też… dodać nową dyskusję, gdy pojawi się nowy wpis!</p>
<h2 id="konfiguracja-akcji"><a class="header-anchor" href="https://blog.comandeer.pl/calkowicie-swobodna-dyskusja#konfiguracja-akcji">Konfiguracja akcji</a></h2>
<p>Żeby móc odpalić akcję, trzeba dodać do repozytorium odpowiedni plik konfiguracyjny, tzw. <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax" rel="noreferrer noopener"><i lang="en">workflow</i></a>. Jest to plik w <a href="https://learnxinyminutes.com/docs/yaml/" rel="noreferrer noopener">formacie YAML</a>, który zawiera informacje, jakie akcje chcemy odpalić oraz kiedy. Ten plik następnie umieszcza się w katalogu <code>.github/workflows</code>.</p>
<p>Dodajmy zatem plik <code>.github/workflows/fix-discussions.yml</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">YAML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">Fix discussions</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">permissions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 2</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  discussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">write</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 4</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    branches</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#032F62;--shiki-dark:#9ECBFF">main</span><span style="color:#6A737D;--shiki-dark:#6A737D"> #5</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    paths</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'src/_posts/**/*.md'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> #6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">jobs</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 7</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">  fix-discussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 8</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    runs-on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">ubuntu-latest</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 9</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">    steps</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#6A737D;--shiki-dark:#6A737D"># 10</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#22863A;--shiki-dark:#85E89D">uses</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 11</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        with</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          fetch-depth</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">2</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 12</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#22863A;--shiki-dark:#85E89D">uses</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 13</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        with</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          node-version</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#005CC5;--shiki-dark:#79B8FF">24</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 14</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#22863A;--shiki-dark:#85E89D">run</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">npm install --no-save @octokit/action</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 15</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">      - </span><span style="color:#22863A;--shiki-dark:#85E89D">run</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">node .github/actions/fix-discussions.mjs</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 16</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">        env</span><span style="color:#24292E;--shiki-dark:#E1E4E8">:</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          GITHUB_TOKEN</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">${{ secrets.GITHUB_TOKEN }}</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 17</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          CATEGORY_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'[ciach]'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 18</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          POSTS_DIR_PATH</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'src/_posts'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 19</span></span>
<span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">          BLOG_URL</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'https://blog.comandeer.pl'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> # 20</span></span>
<span class="line"></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku podajemy nazwę workflowu (1). Następnie określamy <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#permissions" rel="noreferrer noopener">uprawnienia dla domyślnego tokena dostępowego</a> (2). Każdy workflow posiada token, który jest wykorzystywany do autoryzacji wszystkich operacji. W naszym wypadku token musi umożliwiać dodawanie dyskusji (3), więc dostaje tylko takie uprawnienia. Następnie określamy, kiedy workflow ma się wykonać (4). Chcemy, żeby zachodził, gdy zmiany zostaną <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#running-your-workflow-only-when-a-push-to-specific-branches-occurs" rel="noreferrer noopener">wypchnięte do brancha <code>main</code></a> (5) i do tego <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#running-your-workflow-only-when-a-push-affects-specific-files" rel="noreferrer noopener">dotyczyć&nbsp;będą plików wpisów</a> (6). Potem ustalamy, co tak naprawdę ma się&nbsp;stać, tworząc zadania – <code>jobs</code>  (7). W naszym wypadku jest tylko jedno zadanie, <code>fix-discussions</code> (8). <a href="https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idruns-on" rel="noreferrer noopener">Odpalamy je</a> na Linuksie (9). Następnie w <code>steps</code> (10) ustalamy poszczególne kroki zadania. Na początku korzystamy z <a href="https://github.com/actions/checkout" rel="noreferrer noopener">akcji <code>actions/checkout</code></a> (11). Pobiera ona zawartość naszego repozytorium, pozwalając na nim pracować. W naszym przypadku dodatkowo musimy ustawić opcję <code>fetch_depth</code> na <code>2</code> (12). Domyślnie akcja pobiera tylko ostatni commit, a my potrzebujemy dwóch ostatnich (przy logice akcji ten wymóg się&nbsp;wyjaśni). Kolejnym krokiem jest wykorzystanie <a href="https://github.com/actions/setup-node/" rel="noreferrer noopener">akcji <code>actions/setup-node</code></a> w celu instalacji Node.js (13). Zaznaczamy, że chcemy wersję 24 (14). Gdy już Node.js zostanie zainstalowany, instalujemy <a href="https://www.npmjs.com/package/@octokit/action" rel="noreferrer noopener">pakiet npm <code>@octokit/action</code></a> (15). To specjalna odmiana wykorzystywanej przez nas ostatnio biblioteki <a href="https://github.com/octokit" rel="noreferrer noopener">Octokit.js</a>, dostosowana pod GitHub Actions. Na sam koniec uruchamiamy nasz skrypt JS (16), przekazując mu szereg zmiennych środowiskowych: token dostępowy GitHuba (17), ID kategorii dyskusji (18), względną&nbsp;ścieżkę do katalogu z wpisami (19) oraz URL bloga (20).</p>
<div class="note" role="note" aria-labelledby="note-59"><p class="note__label" id="note-59">Dygresja</p><div class="note__content"><p>Jak można zauważyć, po nazwach wykorzystanych akcji znajduje się <code>@&lt;ciąg znaków&gt;</code>. Ten ciąg znaków wskazuje na konkretny commit w repozytorium akcji. Dzięki temu mamy pewność, że <a href="https://docs.github.com/en/actions/reference/security/secure-use#:~:text=Pin%20actions%20to%20a%20full%2Dlength%20commit%20SHA" rel="noreferrer noopener">nasz kod zawsze użyje tej konkretnej wersji akcji</a>. Innymi słowy: nawet jeśli w wyniku ataku ktoś wypuści nową wersję akcji, nasz kod powinien być bezpieczny, bo taką&nbsp;wersję zignoruje.</p></div></div>
<h2 id="logika-akcji"><a class="header-anchor" href="https://blog.comandeer.pl/calkowicie-swobodna-dyskusja#logika-akcji">Logika akcji</a></h2>
<p>Skrypt, będący “mózgiem” naszego workflowu, znajduje się w pliku <code>.github/actions/fix-discussions.mjs</code>. Jego główna logika prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> octokit</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Octokit</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> main</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> fullPostsDirPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> resolvePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">cwd</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(), env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">POSTS_DIR_PATH</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postFiles</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostFiles</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">GITHUB_SHA</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">POSTS_DIR_PATH</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostMetadata</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( postFiles ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> missingPosts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostsWithoutDiscussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		posts,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		repository: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">GITHUB_REPOSITORY</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Brakujących dyskusji:'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, missingPosts.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createMissingDiscussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		repository: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">GITHUB_REPOSITORY</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		categoryId: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">CATEGORY_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		posts: missingPosts,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		blogUrl: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">BLOG_URL</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Całość&nbsp;zamknięta jest w asynchronicznej funkcji <code>main()</code> (1). W gruncie rzeczy jest ona bardzo podobna do wczorajszego kodu. Wewnątrz funkcji <code>getPostMetadata()</code> (2) została zamknięta pętla wyciągająca metadane dla każdego wpisu. Sam <a href="https://blog.comandeer.pl/swobodna-dyskusja#stworzenie-listy-wpis%C3%B3w:~:text=Z%20kolei%20funkcja%20getMetadata()%20prezentuje%20si%C4%99%20nast%C4%99puj%C4%85co">kod je wyciągający</a> jest identyczny. Tak samo praktycznie bez zmian została funkcja tworząca nowe dyskusje, <code>createMissingDiscussions()</code> (3). Jedyną&nbsp;różnicą jest przekazywanie jej zmiennej środowiskowej <code>GITHUB_REPOSITORY</code>, zamiast osobno właściciela i nazwy repozytorium. Ta zmienna środowiskowa ma format <code>&lt;WŁAŚCICIEL&gt;/&lt;REPOZYTORIUM&gt;</code>, więc uzyskanie właściciela i nazwy sprowadza się do <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split" rel="noreferrer noopener">podzielenia</a> wartości zmiennej na znaku <code>/</code>. Zmiany natomiast zaszły w funkcji pobierającej listę plików z wpisami (4) oraz w funkcji sprawdzającej, czy dla podanych postów istnieją dyskusje (5).</p>
<p>Przyjrzyjmy się najpierw funkcji <code>getPostFiles()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { execSync } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:child_process'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostFiles</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">commitHash</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">postsDirPath</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> rawCommitInfo</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> execSync</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`git diff-tree --no-commit-id --name-only ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> commitHash</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } -r`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> commitInfo</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> rawCommitInfo.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">toString</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> lines</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> commitInfo.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">split</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> lines </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">line</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> trimmedLine</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> line.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> trimmedLine.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">startsWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( postsDirPath ) </span><span style="color:#D73A49;--shiki-dark:#F97583">&amp;&amp;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> trimmedLine.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">endsWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'.md'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		} )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> resolvePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">cwd</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(), path.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Funkcja przyjmuje dwa argumenty (1): <a href="https://graphite.com/guides/git-hashing" rel="noreferrer noopener">hash aktualnego commita</a> oraz względną ścieżkę do katalogu z wpisami. Na sam początek wykorzystujemy <a href="https://nodejs.org/api/child_process.html#child_processexecsynccommand-options" rel="noreferrer noopener">funkcję&nbsp;<code>execSync()</code></a> z wbudowanego modułu <code>node:child_process</code> (2), aby wywołać <a href="https://git-scm.com/docs/git-diff-tree" rel="noreferrer noopener">komendę <code>git diff-tree</code></a> na przekazanym hashu commita (3). Komenda ta zwróci nam listę plików zmienionych w danym commicie. A że lista ta jest generowana na podstawie porównania tego commita z poprzednimi, stąd potrzeba wymuszenia na akcji <code>actions/checkout</code> pobrania dwóch ostatnich commitów (domyślnie pobiera tylko ostatni). Funkcja <code>execSync()</code> zwróci nam wynik w postaci <a href="https://nodejs.org/api/buffer.html" rel="noreferrer noopener">bufora</a>, więc musimy go <a href="https://nodejs.org/api/buffer.html#buftostringencoding-start-end" rel="noreferrer noopener">skonwertować na ciąg znaków</a> (4). Z racji tego, że <code>git diff-tree</code> wyświetla jedną nazwę pliku w linii, musimy podzielić wynik tej komendy na poszczególne linie (5). Następnie filtrujemy tablicę linii i znajdujemy tylko te, które dotyczą plików z wpisami (6). Następnie dla każdego wpisu <a href="https://nodejs.org/api/path.html#pathresolvepaths" rel="noreferrer noopener">rozwiązujemy pełną ścieżkę</a> (7). Tak spreparowaną&nbsp;tablicę ścieżek zwracamy z funkcji (8).</p>
<p>Przejdźmy teraz do funkcji <code>getPostsWithoutDiscussions()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getPostsWithoutDiscussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">options</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postsWithoutDiscussions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> []; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> post</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> options.posts ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> isMissingDiscussion</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( options.repository, post.slug ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			postsWithoutDiscussions.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( post ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sleep</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">getRandomNumber</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">5</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> postsWithoutDiscussions; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku tworzymy w niej tablicę wpisów, które nie mają dyskusji (1). Następnie iterujemy po wszystkich przekazanych postach (2) i sprawdzamy, czy każdy z nich ma swoją&nbsp;dyskusję (3). Jeśli nie, trafia do tablicy (4). Następnie odczekujemy losową liczbę sekund (5), żeby nie nadziać&nbsp;się na limity GitHub API. Jak już przerobimy wszystkie posty, zwracamy naszą tablicę (6).</p>
<p>Przyjrzyjmy się więc funkcji <code>isMissingDiscussion()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> isMissingDiscussion</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">repository</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">title</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> searchQuery</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `repo:${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> repository</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> } in:title "${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> title</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }"`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">	/* 1 */</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> graphqlQuery</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		query($searchQuery: String!) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			search(query: $searchQuery, type: DISCUSSION, first: 1) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				edges {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					node {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">						... on Discussion {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">							title</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">						}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">	`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> octokit.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">graphql</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( graphqlQuery, {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		searchQuery</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> discussions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> result.search.edges.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">map</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">edge</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> edge.node;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> discussions.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">findIndex</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">discussion</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8">  {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> discussion.title </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> title; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} ) </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#D73A49;--shiki-dark:#F97583"> -</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zawiera ona zapytanie GraphQL (1), które korzysta z wyszukiwarki GitHuba, żeby znaleźć&nbsp;dyskusję o konkretnym tytule. Samo zapytanie do wyszukiwarki (2) można… testować w wyszukiwarce GitHuba – <a href="https://docs.github.com/en/search-github/searching-on-github/searching-discussions#search-by-the-title-body-or-comments" rel="noreferrer noopener">składnia jest identyczna</a>. Po otrzymaniu wyniku zapytania z API, sprawdzamy, czy znajduje się&nbsp;w nim rekord dla naszego wpisu (3). Jeśli tak, funkcja zwróci <code>false</code>. W innym wypadku zwróci <code>true</code>. Dzieje się&nbsp;tak, ponieważ <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex" rel="noreferrer noopener">metoda <code>Array#findIndex()</code></a> zwróci <code>-1</code> (4) tylko wtedy, gdy szukanej rzeczy nie ma w tablicy.</p>
<p>I to w sumie tyle, reszta logiki skryptu działa praktycznie identycznie jak tego wczorajszego. Ale dzięki przeniesieniu tej logiki do akcji, dyskusje dla nowych wpisów powinny dodawać się automatycznie. Jak choćby <a href="https://github.com/Comandeer/blog/discussions/categories/komentarze?discussions_q=category%3AKomentarze+%2Fcalkowicie-swobodna-dyskusja" rel="noreferrer noopener">dla <em>tego</em> wpisu</a>!</p>
<p><small>A jeśli powyższy link jednak nie działa, to jutro zapewne kolejna część&nbsp;tej sagi…</small></p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">Swobodna dyskusja</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/swobodna-dyskusja" rel="alternate" type="text/html"/>
			<published>2025-12-06T23:20:00.000Z</published>
			<updated>2025-12-06T23:20:00.000Z</updated>
			<id>https://blog.comandeer.pl/swobodna-dyskusja</id>
			
				<summary><![CDATA[Okazało się, że komentarze na blogu nie u wszystkich działają. Postanowiłem się przyjrzeć temu problemowi.]]></summary>
			
			<content type="html"><![CDATA[<p>Ostatnio dostałem <a href="https://github.com/Comandeer/blog/discussions/51" rel="noreferrer noopener">zgłoszenie błędu w komentarzach na blogu</a>. W skrócie: jeśli nie zaakceptowało się&nbsp;ciasteczek, nie dało się dodać komentarza. Postanowiłem przyjrzeć się temu problemowi i znaleźć&nbsp;jakieś rozwiązanie.</p>
<h2 id="jak-działają-komentarze"><a class="header-anchor" href="https://blog.comandeer.pl/swobodna-dyskusja#jak-działają-komentarze">Jak działają komentarze?</a></h2>
<p>Na swojej stronie używam systemu <a href="https://giscus.app/pl" rel="noreferrer noopener">Giscus</a>. Pozwala on dodawać komentarze w formie <a href="https://docs.github.com/en/discussions" rel="noreferrer noopener">dyskusji na GitHubie</a>. Każdy wpis ma swoją własną dyskusję. Tak dodane komentarze są wyświetlone na dole, pod każdym wpisem:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/swobodna-dyskusja/giscus-728w.avif"><picture><source type="image/avif" srcset="/assets/images/swobodna-dyskusja/giscus-440w.avif 440w, /assets/images/swobodna-dyskusja/giscus-728w.avif 728w" sizes="90vw"><source type="image/webp" srcset="/assets/images/swobodna-dyskusja/giscus-440w.webp 440w, /assets/images/swobodna-dyskusja/giscus-728w.webp 728w" sizes="90vw"><source type="image/png" srcset="/assets/images/swobodna-dyskusja/giscus-440w.png 440w, /assets/images/swobodna-dyskusja/giscus-728w.png 728w" sizes="90vw"><img src="/assets/images/swobodna-dyskusja/giscus-728w.png" width="728" height="547" style="aspect-ratio: 728 / 547" alt="Sekcja &quot;Komentarze&quot;, w której znajduje się informacja o tym, że jest 1 komentarz, możliwość sortowania komentarzy od najstarszego lub najnowszego, a następnie formularz dodawania nowych komentarzy, po którym następuje lista dodanych komentarzy. Jest tylko jeden komentarz, dodany 1 sierpnia 2024 przez look997 o treści &quot;Szkoda że nie SvelteKit :D&quot;." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Przykład z wpisu <a href="https://blog.comandeer.pl/blog-2.0">Blog 2.0</a></p></figcaption></figure>
<p>Działa to całkiem przyzwoicie. Minusem jest wymóg, aby każda komentująca osoba miała konto na GitHubie. Wydaje mi się jednak, że z uwagi na grupę docelową bloga jest to akceptowalny kompromis. Jest też drugi minus: Giscus wymaga zarówno JS-a, jak i zaakceptowania ciasteczek. Co się stanie, jeśli zabraknie choć jednego z nich? Zamiast komentarzy, pojawi się link do dyskusji na GitHubie:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/swobodna-dyskusja/no-js-725w.avif"><picture><source type="image/avif" srcset="/assets/images/swobodna-dyskusja/no-js-440w.avif 440w, /assets/images/swobodna-dyskusja/no-js-725w.avif 725w" sizes="90vw"><source type="image/webp" srcset="/assets/images/swobodna-dyskusja/no-js-440w.webp 440w, /assets/images/swobodna-dyskusja/no-js-725w.webp 725w" sizes="90vw"><source type="image/png" srcset="/assets/images/swobodna-dyskusja/no-js-440w.png 440w, /assets/images/swobodna-dyskusja/no-js-725w.png 725w" sizes="90vw"><img src="/assets/images/swobodna-dyskusja/no-js-725w.png" width="725" height="154" style="aspect-ratio: 725 / 154" alt="Sekcja &quot;Komentarze&quot;, w której znajduje się link &quot;Przejdź do komentarzy bezpośrednio na GitHubie&quot;" loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Kliknij obrazek, aby go powiększyć</p></figcaption></figure>
<p>I właśnie tutaj pojawia się zgłoszony błąd! W przypadku, gdy dany post ma już jakieś komentarze, link zaprowadzi nas do istniejącej dyskusji. Jednak, gdy post dotąd nie został skomentowany, dana dyskusja nie istnieje, a komentująca osoba nie jest w stanie jej sama stworzyć. Giscus robi to automatycznie przy pierwszym komentarzu. A że nie chcę odcinać nawet części czytających od możliwości komentowania, stwierdziłem, że zainteresuję się problemem i postaram znaleźć&nbsp;jakieś rozwiązanie.</p>
<h2 id="częściowe-rozwiązanie"><a class="header-anchor" href="https://blog.comandeer.pl/swobodna-dyskusja#częściowe-rozwiązanie">(Częściowe) rozwiązanie</a></h2>
<p>Logicznie nasuwającym się rozwiązaniem jest stworzenie brakujących dyskusji – tak, aby dla każdego wpisu istniała jedna. Tym zajmiemy się dzisiaj! Napiszemy skrypt w Node.js, który:</p>
<ol>
<li>stworzy listę wszystkich wpisów,</li>
<li>sprawdzi, czy istnieją dla nich dyskusje,</li>
<li>stworzy wszystkie brakujące dyskusje.</li>
</ol>
<p>To rozwiązanie jednak naprawia tylko część problemu. Dyskusje powstaną bowiem dla już&nbsp;istniejących wpisów. Musiałbym regularnie odpalać ten skrypt, żeby wykrywać nowe wpisy i dodawać do nich dyskusje. Wygodniejszym rozwiązaniem byłoby automatyczne tworzenie dyskusji w momencie, gdy nowy wpis zostaje wypchany do repozytorium. Tym zajmiemy się kiedy indziej. Na razie skupimy się na naprawieniu dyskusji dla już istniejących wpisów.</p>
<h3 id="stworzenie-listy-wpisów"><a class="header-anchor" href="https://blog.comandeer.pl/swobodna-dyskusja#stworzenie-listy-wpisów">Stworzenie listy wpisów</a></h3>
<p>Struktura plików bloga jest w miarę uporządkowana. Wszystkie wpisy znajdują się w <a href="https://github.com/Comandeer/blog/tree/dfb6e15de893aec17384e483c984eb189ff2531e/src/_posts" rel="noreferrer noopener">katalogu <code>src/_posts</code></a>, w podkatalogu odpowiedniej kategorii. Same wpisy to pliki <code>.md</code>. Zatem wyciągnięcie listy wpisów w dużej mierze sprowadza się do wyszukania wszystkich plików <code>.md</code> w katalogu <code>src/_posts</code>. Dokładnie do takich zadań został stworzony <a href="https://en.wikipedia.org/wiki/Glob_(programming)" rel="noreferrer noopener">glob</a>! W Node.js od wersji 22 istnieje <a href="https://nodejs.org/api/fs.html#fspromisesglobpattern-options" rel="noreferrer noopener">odpowiednia funkcja</a> od tego. Wystarczy ją wykorzystać do wyciągnięcia listy plików <code>.md</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { glob } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:fs/promises'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postsDirPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> '[ciach]blog/src/_posts'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postFiles</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> glob</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'**/*.md'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	cwd: postsDirPath </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">for</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postFile</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8">  postFiles ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( postFile ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku importujemy funkcję <code>glob()</code> z wbudowanego modułu <code>node:fs</code> (1). Następnie wywołujemy ją ze wzorcem <code>**/*.md</code> (2) oraz katalogiem (3) ustawionym na absolutną ścieżkę do katalogu z wpisami bloga (4). Następnie w pętli (5) wyświetlamy wszystkie pliki (6). Powinniśmy uzyskać mniej więcej taką listę:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>standardy-sieciowe/2017-03-19-drzewko-dostepnosci-udostepnione.md</span></span>
<span class="line"><span>standardy-sieciowe/2017-04-02-web-components-koszmar-minionego-lata.md</span></span>
<span class="line"><span>standardy-sieciowe/2017-04-09-potrzebujemy-zachowan-nie-dziedziczenia.md</span></span>
<span class="line"><span>standardy-sieciowe/2018-01-05-pyrrusowe-zwyciestwo.md</span></span>
<span class="line"><span>standardy-sieciowe/2018-01-23-zawieszenie-broni.md</span></span>
<span class="line"><span>[…]</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Jak widać, uzyskaliśmy ścieżki do plików względem katalogu z wpisami. Wróćmy jeszcze na chwilę do samego wzorca:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>**/*.md</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Nie będę wchodził tutaj w szczegóły składni globa, ale ten wzorzec oznacza “wszystkie pliki z rozszerzeniem <code>.md</code>, w dowolnym podkatalogu obecnego katalogu”. Gdybyśmy ograniczyli się tylko do <code>*.md</code>, glob nic by nie znalazł (nie wszedłby do żadnego z podkatalogów kategorii).</p>
<p>No dobrze, ale surowa lista plików to jedynie połowa sukcesu. Zobaczmy, jak wygląda <a href="https://github.com/Comandeer/blog/discussions/30" rel="noreferrer noopener">przykładowa dyskusja</a> stworzona przez Giscusa:</p>
<figure class="figure"><a class="figure__link" href="/assets/images/swobodna-dyskusja/giscus-728w.avif"><picture><source type="image/avif" srcset="/assets/images/swobodna-dyskusja/giscus-440w.avif 440w, /assets/images/swobodna-dyskusja/giscus-728w.avif 728w" sizes="90vw"><source type="image/webp" srcset="/assets/images/swobodna-dyskusja/giscus-440w.webp 440w, /assets/images/swobodna-dyskusja/giscus-728w.webp 728w" sizes="90vw"><source type="image/png" srcset="/assets/images/swobodna-dyskusja/giscus-440w.png 440w, /assets/images/swobodna-dyskusja/giscus-728w.png 728w" sizes="90vw"><img src="/assets/images/swobodna-dyskusja/giscus-728w.png" width="728" height="547" style="aspect-ratio: 728 / 547" alt="Na górze znajduje się tytuł dyskusji, &quot;/blog-2.0&quot;, pod którym znajduje się pierwszy wpis dodany przez bota Giscusa. Wpis również ma tytuł &quot;/blog-2.0&quot;, pod którym znajduje się opis postu na blogu (&quot;Dostępna jest już nowa, odświeżona wersja bloga!&quot;), a następnie – link do wpisu." loading="lazy" decoding="async"></picture></a><figcaption class="figure__caption"><p>Dyskusja dla wpisu <a href="https://blog.comandeer.pl/blog-2.0">Blog 2.0</a></p></figcaption></figure>
<p>Tak naprawdę potrzebujemy <em>pomrowa</em>…! Czyli <a href="https://developer.mozilla.org/en-US/docs/Glossary/Slug" rel="noreferrer noopener">sluga</a>, a więc – ostatnią&nbsp;część URL-a wpisu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>https://blog.comandeer.pl/blog-2.0</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>W przypadku mojego bloga slugiem jest wszystko to, co następuje po domenie, a więc – <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Path" rel="noreferrer noopener">ścieżka</a>.</p>
<p>Tę możemy uzyskać z tzw. <a href="https://www.11ty.dev/docs/data-frontmatter/" rel="noreferrer noopener"><i lang="en">front matter</i> (strony tytułowej)</a>.  To sekcja na początku pliku <code>.md</code>, wydzielona przy pomocy <code>---</code> i zawierająca metadane wpisu w <a href="https://learnxinyminutes.com/yaml/" rel="noreferrer noopener">formacie YAML</a>. W przypadku mojego bloga jedną z metadanych jest <code>permalink</code>, a więc – <a href="https://www.11ty.dev/docs/permalinks/" rel="noreferrer noopener">ostateczna nazwa wynikowego pliku HTML</a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">YAML</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#22863A;--shiki-dark:#85E89D">permalink</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">/blog-2.0.html</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>A stąd już prosta droga do ścieżki, która – dzięki konfiguracji backendu – pomija rozszerzenie <code>.html</code>. Wystarczy zatem… sparsować YAML-a, wyciągnąć z niego wartość danej <code>permalink</code> i obciąć&nbsp;z niej <code>.html</code>! A jak już to będziemy robić, to warto przy okazji zainteresować się inną metadaną, <code>comments</code>, która określa, czy dany wpis powinien mieć komentarze, czy nie. Jak na razie tylko <a href="https://blog.comandeer.pl/pamiec-internetu">jeden wpis nie ma komentarzy</a>, ale nie wykluczam, że pojawią się kolejne w przyszłości. Możemy też od razu wyciągnąć daną&nbsp;<code>description</code>, czyli opis wpisu, który przyda nam się przy tworzeniu dyskusji.</p>
<p>Sparsujmy zatem YAML-a! Na całe szczęście nie musimy dokładnie parsować całej strony tytułowej, wystarczy nam wyszukanie w niej trzech danych. A do tego wykorzystać możemy <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions" rel="noreferrer noopener">wyrażenia regularne</a>. Przystąpmy do pracy:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { glob, readFile } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:fs/promises'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { resolve </span><span style="color:#D73A49;--shiki-dark:#F97583">as</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> resolvePath } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:path'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> posts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> []; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">for</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postFile</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8">  postFiles ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postFilePath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> resolvePath</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( postsDirPath, postFile ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postContent</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> readFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( postFilePath, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'utf-8'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postMetadata</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getMetadata</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( postContent ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( postMetadata.comments ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		posts.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( postMetadata ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku importujemy dwie dodatkowe funkcje: <a href="https://nodejs.org/api/fs.html#fspromisesreadfilepath-options" rel="noreferrer noopener"><code>readFile()</code></a> (1) do odczytywania zawartości plików oraz <a href="https://nodejs.org/api/path.html#pathresolvepaths" rel="noreferrer noopener"><code>resolve()</code></a> (2) do rozwiązywania pełnych ścieżek do plików. Następnie tworzymy tablicę <code>posts</code> (3), w której będziemy przechowywać metadane wpisów zezwalających na komentarze. Z kolei w pętli rozwiązujemy pełną ścieżkę pliku wpisu względem katalogu z wpisami (4), a następnie używamy jej do odczytania zawartości pliku (5). Tę zawartość przekazujemy funkcji <code>getMetadata()</code> (6), której zadaniem jest wyciągnięcie metadanych ze strony tytułowej. Jeśli w metadanych jest informacja, że wpis zezwala na komentarze (7), metadane są umieszczane w tablicy <code>posts</code> (8).</p>
<p>Z kolei funkcja <code>getMetadata()</code> prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> FRONT_MATTER_REGEX</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#D73A49;--shiki-dark:#F97583">^</span><span style="color:#032F62;--shiki-dark:#DBEDFF">---</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#DBEDFF">(?&lt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8">content</span><span style="color:#032F62;--shiki-dark:#DBEDFF">&gt;(?:</span><span style="color:#005CC5;--shiki-dark:#79B8FF">.</span><span style="color:#D73A49;--shiki-dark:#F97583">|</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#DBEDFF">)</span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#032F62;--shiki-dark:#DBEDFF">)</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#DBEDFF">---</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#D73A49;--shiki-dark:#F97583">u</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getMetadata</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">content</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> frontMatter</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> content.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">match</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">FRONT_MATTER_REGEX</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> lines</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> frontMatter.groups.content.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">split</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#005CC5;--shiki-dark:#79B8FF">\n</span><span style="color:#032F62;--shiki-dark:#9ECBFF">'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> lines.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">reduce</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">metadata</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">line</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		[…]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> metadata;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}, {} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match" rel="noreferrer noopener">wyszukujemy</a> stronę tytułową w treści wpisu (1) przy pomocy wyrażenia regularnego (2). Szuka ono ciągu, który zaczyna się od trzech myślników (<code>-</code>), po których następuje znak nowej linii (<code>\n</code>), po niej interesująca nas treść, którą zapisujemy do grupy <code>content</code>, a po treści następuje znowu znak nowej linii (<code>\n</code>) oraz ponownie trzy myślniki. Następnie treść strony tytułowej dzielimy na poszczególne linie (3). Tak uzyskaną&nbsp;tablicę&nbsp;z treścią każdej linii <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce" rel="noreferrer noopener">przekształcamy</a> na obiekt z metadanymi (4).</p>
<p>Z kolei same przekształcenia zależą od tego, jaką linię akurat parsujemy. Rozpoznamy to poprzez sprawdzenie, czy jej treść <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith" rel="noreferrer noopener">zaczyna się</a> od nazwy poszukiwanej przez nas metadanej:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( line.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">startsWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'&lt;nazwa metadanej&gt;'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">) ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">	// zrób coś</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Dla metadanej <code>permalink</code> kod wygląda następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> PERMALINK_TRIM_REGEX</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold">\.</span><span style="color:#032F62;--shiki-dark:#DBEDFF">html</span><span style="color:#D73A49;--shiki-dark:#F97583">$</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#D73A49;--shiki-dark:#F97583">gu</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( line.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">startsWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'permalink'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [ , </span><span style="color:#005CC5;--shiki-dark:#79B8FF">lineContent</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> line.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">split</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">':'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        ...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">metadata, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        slug: lineContent.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">PERMALINK_TRIM_REGEX</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    }</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Sprawdzamy, czy mamy do czynienia z odpowiednią daną (1). Następnie <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split" rel="noreferrer noopener">dzielimy</a> linię na dwie części (2): niepotrzebną nam nazwę metadanej oraz jej wartość. Dzielenie jest robione na znaku dwukropka (<code>:</code>). Następnie zwracamy obiekt z metadanymi. Kopiujemy już istniejące (3), po czym dodajemy własność <code>slug</code> (4). Jej wartością jest wartość metadanej, <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim" rel="noreferrer noopener">pozbawiona białych znaków z początku i końca</a>, oraz z usuniętym niepotrzebnym rozszerzeniem <code>.html</code> z końca. Usunięcia dokonaliśmy przez <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace" rel="noreferrer noopener">zastąpienie</a> rozszerzenia, wyszukanego wyrażeniem regularnym (5), przez pusty ciąg.</p>
<p>Podobnie wygląda to w przypadku metadanej <code>description</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> DESCRIPTION_TRIM_REGEX</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> /</span><span style="color:#D73A49;--shiki-dark:#F97583">^</span><span style="color:#032F62;--shiki-dark:#DBEDFF">"</span><span style="color:#D73A49;--shiki-dark:#F97583">|</span><span style="color:#032F62;--shiki-dark:#DBEDFF">"</span><span style="color:#D73A49;--shiki-dark:#F97583">$</span><span style="color:#032F62;--shiki-dark:#9ECBFF">/</span><span style="color:#D73A49;--shiki-dark:#F97583">gu</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( line.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">startsWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'description'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [ , </span><span style="color:#005CC5;--shiki-dark:#79B8FF">lineContent</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> line.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">split</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">':'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        ...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">metadata,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        description: lineContent.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="color:#24292E;--shiki-dark:#E1E4E8">().</span><span style="color:#6F42C1;--shiki-dark:#B392F0">replace</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">DESCRIPTION_TRIM_REGEX</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">''</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> )</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jedyna różnica polega na tym, że w tym przypadku zamiast rozszerzenia <code>.html</code> usuwamy cudzysłowy (1), którymi otoczona jest większość opisów.</p>
<p>Do tego jeszcze dorzucamy obsługę metadanej <code>comments</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( line.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">startsWith</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'comments'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [ , </span><span style="color:#005CC5;--shiki-dark:#79B8FF">lineContent</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ] </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> line.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">split</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">':'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">    return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">        ...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">metadata,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        comments: lineContent.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">trim</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'true'</span><span style="color:#D73A49;--shiki-dark:#F97583"> ?</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="color:#D73A49;--shiki-dark:#F97583"> :</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    };</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W tym przypadku zamiast zawartości metadanej, zwracamy <code>true</code> albo <code>false</code> –&nbsp;w zależności od tego, czy metadana miała wartość&nbsp;<code>true</code> lub <code>false</code> (1). Z racji tego, że parsujemy YAML-a ręcznie, cała zawartość&nbsp;strony tytułowej jest tekstem, więc musimy tę&nbsp;konwersję zrobić sami.</p>
<div class="note" role="note" aria-labelledby="note-58"><p class="note__label" id="note-58">Dygresja</p><div class="note__content"><p>Niestety, proste <code>Boolean( wartoscMetadanej )</code> nie wystarczy, bo każdy niepusty ciąg tekstowy (w tym <code>'false'</code>) zwraca <code>true</code>.</p></div></div>
<p>I w końcu mamy listę wszystkich wpisów, które mają mieć komentarze! Teraz pora przystąpić do drugiej części zadania.</p>
<h3 id="sprawdzenie-czy-dla-wpisów-istniejądyskusje"><a class="header-anchor" href="https://blog.comandeer.pl/swobodna-dyskusja#sprawdzenie-czy-dla-wpisów-istniejądyskusje">Sprawdzenie, czy dla wpisów istnieją&nbsp;dyskusje</a></h3>
<p>Sprawdzenie, jakie dyskusje istnieją w repozytorium bloga, wymaga użycia <a href="https://docs.github.com/en/graphql/guides/using-the-graphql-api-for-discussions" rel="noreferrer noopener">API GitHuba</a>. Na całe szczęście, dla Node.js istnieje <a href="https://github.com/octokit/octokit.js" rel="noreferrer noopener">biblioteka Octokit.js</a>, która zdecydowanie to ułatwia. Wypada ją&nbsp;zatem zainstalować:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Bash</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">npm</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> install</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> octokit</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Żeby jednak móc ją użyć do połączenia się z API, trzeba stworzyć <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token" rel="noreferrer noopener">nowy token dostępowy</a>. Najlepiej, żeby miał jak najmniejsze uprawnienia. W tym przypadku: dostęp tylko do repozytorium bloga, a w nim – tylko do dyskusji.</p>
<p>Mimo minimalnych uprawnień, taki token najlepiej trzymać poza kodem, np. w <a href="https://dotenvx.com/docs/env-file" rel="noreferrer noopener">pliku <code>.env</code></a>. Jest to na tyle popularna technika, że Node od wersji 20.12 ma wbudowane funkcje w celu jej wspierania. Zacznijmy od stworzenia pliku <code>.env</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage"></span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span>GITHUB_TOKEN="[ciach]" # 1</span></span>
<span class="line"><span>POSTS_DIR_PATH="[ciach]/blog/src/_posts" # 2</span></span>
<span class="line"><span>REPO_OWNER="Comandeer" # 3</span></span>
<span class="line"><span>REPO_NAME="blog" # 4</span></span>
<span class="line"><span>CATEGORY_ID="[ciach]" # 5</span></span>
<span class="line"><span></span></span></code></pre>

			</div>
		</figure><p>Stworzyliśmy w nim sześć zmiennych:</p>
<ol>
<li><code>GITHUB_TOKEN</code> – token dostępowy GitHuba,</li>
<li><code>POST_DIR_PATH</code> – ścieżkę do katalogu z postami,</li>
<li><code>REPO_OWNER</code> – nazwę właściciela repozytorium na GitHubie,</li>
<li><code>REPO_NAME</code> – nazwę repozytorium na GitHubie,</li>
<li><code>CATEGORY_ID</code> – ID kategorii na GitHub Discussions (można użyć Giscusa, żeby je uzyskać),</li>
<li><code>BLOG_URL</code> – adres bloga (bez <code>/</code> na końcu).</li>
</ol>
<p>Dzięki temu całość konfiguracji skryptu jest poza skryptem i nie trzeba będzie dłużej zmieniać kodu, żeby np. zaktualizować ścieżkę. A i trzymanie tego w zewnętrznym pliku sprawia, że jest zdecydowanie mniejsza szansa, że przez przypadek wypchamy do repo token.</p>
<p>Pora wczytać zawartość&nbsp;pliku <code>.env</code> do zmiennych środowiskowych:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { loadEnvFile, env } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:process'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { Ocktokit } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'octokit'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">loadEnvFile</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> postsDirPath</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">POSTS_DIR_PATH</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Najpierw importujemy <a href="https://nodejs.org/api/process.html#processloadenvfilepath" rel="noreferrer noopener">funkcję&nbsp;<code>loadEnvFile()</code></a> oraz <a href="https://nodejs.org/api/process.html#processenv" rel="noreferrer noopener">zmienną&nbsp;<code>env</code></a> z <code>node:process</code> (1). Następnie wywołujemy <code>loadEnvFile()</code> (2), żeby stworzyć&nbsp;zmienne środowiskowe na podstawie pliku <code>.env</code>. Na końcu używamy tak wczytanej zmiennej środowiskowej do określenia położenia katalogu z wpisami (3). Przy okazji możemy też od razu zaimportować bibliotekę <code>octokit</code> (4).</p>
<p>Pora stworzyć funkcję, która pobierze dla nas wszystkie dyskusje z kategorii przeznaczonej na komentarze:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> octokit</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> Octokit</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	auth: process.env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">GITHUB_TOKEN</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 10</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getExistingDiscussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">options</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">	/* 1 */</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> query</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		query($owner: String!, $repo: String!, $categoryId: ID!, $cursor: String) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			repository(owner: $owner, name: $repo) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				discussions(</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					first: 100,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					after: $cursor,</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					categoryId: $categoryId</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					nodes {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">						title</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					pageInfo {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">						hasNextPage</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">						endCursor</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> discussions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> [];</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> hasNextPage </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	let</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> cursor; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	do</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> octokit.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">graphql</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( query, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">options,</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			cursor</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		});</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#005CC5;--shiki-dark:#79B8FF">nodes</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#005CC5;--shiki-dark:#79B8FF">pageInfo</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> result.repository.discussions; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 11</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		discussions.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#D73A49;--shiki-dark:#F97583">...</span><span style="color:#24292E;--shiki-dark:#E1E4E8">nodes ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 12</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		hasNextPage </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> pageInfo.hasNextPage; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		cursor </span><span style="color:#D73A49;--shiki-dark:#F97583">=</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> pageInfo.endCursor; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} </span><span style="color:#D73A49;--shiki-dark:#F97583">while</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( hasNextPage ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> discussions; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 13</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Funkcja wygląda dość&nbsp;przerażająco – ale to głównie z uwagi na spore zapytanie GraphQL (1). Nie będę wchodził w szczegóły tego języka. W dużym skrócie: to zapytanie pobierze nam wszystkie dyskusje, które są w podanej kategorii dyskusji. Reszta funkcji to obsługa tego zapytania. Tworzymy pętlę <code>do</code>/<code>while</code> (2), która wykona się&nbsp;co najmniej raz, a kolejne wywołania zależeć będą od tego, czy zmienna <code>hasNextPage</code> jest równa <code>true</code> (3). Ta zmienna (4) przechowuje informacje o <a href="https://docs.github.com/en/graphql/guides/using-pagination-in-the-graphql-api" rel="noreferrer noopener">paginacji</a>. W przypadku bowiem, gdy dyskusji będzie odpowiednio dużo, jedno zapytanie nie zwróci ich wszystkich. Wówczas wynik będzie zawierał informację o tym, że jeszcze są inne dyskusje, które trzeba pobrać (5). W tym celu wynik zawiera także kursor (6), który sobie zapisujemy (7), aby użyć go w kolejnym zapytaniu. Kursor to wskaźnik, który pokazuje, gdzie zakończyliśmy pobieranie dyskusji i pozwala kontynuować od tego miejsca. Samo zapytanie wykonujemy przy pomocy metody <code>#graphl()</code> z biblioteki octokit (8). Najpierw jednak musimy stworzyć klienta Ocktokit (9), przekazując mu token dostępowy (10). Gdy już&nbsp;wykonamy zapytanie, wyciągamy z wyniku dwie własności (11): <code>nodes</code> (zwrócone dyskusje) oraz <code>pageInfo</code> (informacje o paginacji). Zwrócone dyskusje dodajemy do tablicy <code>discussions</code> (12), którą na końcu zwracamy (13).</p>
<p>Teraz trzeba dodać wywołanie funkcji <code>getExistingDiscussions()</code> do istniejącej logiki:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> existingDiscussions</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getExistingDiscussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	owner: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">REPO_OWNER</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	repo: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">REPO_NAME</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	categoryId: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">CATEGORY_ID</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> missingPosts</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> posts.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">filter</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( { </span><span style="color:#E36209;--shiki-dark:#FFAB70">slug</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> existingDiscussions.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">findIndex</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( { </span><span style="color:#E36209;--shiki-dark:#FFAB70">title</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> title </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> slug; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} ) </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#D73A49;--shiki-dark:#F97583"> -</span><span style="color:#005CC5;--shiki-dark:#79B8FF">1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Brakujących dyskusji:'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, missingPosts.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">length</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Umieszczamy ten kod po pętli pobierającej metadane wpisów. Do zmiennej <code>existingDiscussions</code> zapisujemy wynik funkcji <code>getExistingDiscussions()</code> (1). Do jej wywołania przekazujemy zmienne&nbsp;środowiskowe z nazwą właściciela repozytorium (2), nazwą repozytorium (3) oraz ID kategorii dyskusji (4). Następnie tworzymy tablicę (5) wpisów, dla których nie ma jeszcze dyskusji. Robimy to przy pomocy <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter" rel="noreferrer noopener">przefiltrowania</a> tablicy wszystkich wpisów przy pomocy <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex" rel="noreferrer noopener">metody <code>Array#findIndex()</code></a>, sprawdzającej (6), czy tablica istniejących dyskusji posiada wpis dla danego sluga (7). Na końcu wyświetlamy informację, ile dyskusji brakuje (8).</p>
<p>Drugi etap pracy za nami, pora na trzeci!</p>
<h3 id="tworzenie-brakujących-dyskusji"><a class="header-anchor" href="https://blog.comandeer.pl/swobodna-dyskusja#tworzenie-brakujących-dyskusji">Tworzenie brakujących dyskusji</a></h3>
<p>Żeby stworzyć&nbsp;brakujące dyskusje, napiszemy nową funkcję, <code>createMissingDiscussions()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createMissingDiscussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">options</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> repositoryId</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getRepositoryId</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( options.owner, options.repo ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> post</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> options.posts ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createDiscussion</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			repositoryId, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			categoryId: options.categoryId, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			title: post.slug, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			body: </span><span style="color:#6F42C1;--shiki-dark:#B392F0">createDiscussionBody</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( post, options.blogUrl ) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> randomSleepTime</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> Math.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">floor</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( Math.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">random</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() </span><span style="color:#D73A49;--shiki-dark:#F97583">*</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 6</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">+</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sleep</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( randomSleepTime ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku pobieramy ID repozytorium na podstawie jego właściciela i nazwy (1). Następnie dla każdego z przekazanych wpisów (2) wywołujemy funkcję <code>createDiscussion()</code> (3), której przekazujemy ID repozytorium (4), ID kategorii dyskusji (5), tytuł dyskusji, czyli w naszym wypadku sluga (6), oraz treść&nbsp;dyskusji, którą tworzymy przy pomocy funkcji <code>createDiscussionBody()</code> (7). Zanim przejdziemy do dodawania kolejnej dyskusji, odczekujemy losową liczbę sekund (8), a dokładniej – między 1 a 6 (9). Robimy to, żeby nie natknąć się na <a href="https://docs.github.com/en/graphql/overview/rate-limits-and-query-limits-for-the-graphql-api#staying-under-the-rate-limit" rel="noreferrer noopener">ograniczenia po stronie GitHubowego API</a>.</p>
<p>Funkcja <code>getRepositoryId()</code> prezentuje się następująco:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> getRepositoryId</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">owner</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">name</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">	/* 1 */</span><span style="color:#D73A49;--shiki-dark:#F97583"> const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> query</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		query($owner: String!, $name: String!) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			repository(owner: $owner, name: $name) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				id</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> result</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> octokit.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">graphql</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( query, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        owner, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">        name </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    } );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> result.repository.id; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Zawiera ono zapytanie GraphQL (1), które pobiera ID repozytorium. Wykonujemy je (2), przekazując właściciela (3) oraz nazwę repozytorium (4), a następnie zwracamy pobrane ID (5).</p>
<p>Funkcja <code>createDiscussion()</code> z kolei wygląda tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createDiscussion</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">options</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> query</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		mutation($input: CreateDiscussionInput!) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			createDiscussion(input: $input) {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				discussion {</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">					title</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">				}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">			}</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> response</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> await</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> octokit.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">graphql</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( query, { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		input: options </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`Created discussion`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, response.createDiscussion.discussion.title ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Ona również ma w sobie zapytanie GraphQL (1) – z tym, że typu <code>mutation</code> zamiast <code>query</code>. Wykonujemy je (2), przekazując obiekt z danymi nowej dyskusji jako <code>input</code> (3). Na końcu wyświetlamy informację o dodanej dyskusji (4).</p>
<p>Dwie pomocnicze funkcje, <code>createDiscussionBody()</code> i <code>sleep()</code> prezentują się jak poniżej:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createDiscussionBody</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#E36209;--shiki-dark:#FFAB70">slug</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">description</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> }, </span><span style="color:#E36209;--shiki-dark:#FFAB70">blogUrl</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> link</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> blogUrl</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">	/* 1 */</span><span style="color:#D73A49;--shiki-dark:#F97583"> return</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> `# ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> slug</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> description</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }</span></span>
<span class="line"></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">${</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> link</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> }`</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">async</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> sleep</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">seconds</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> Promise</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">resolve</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		setTimeout</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( resolve, seconds </span><span style="color:#D73A49;--shiki-dark:#F97583">*</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 1000</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli chodzi o funkcję <code>sleep()</code>, to bardzo podobną funkcję tworzyliśmy dla <a href="https://blog.comandeer.pl/projektujemy-czasomierze">czasomierzy</a>. Natomiast funkcja <code>createDiscussionBody()</code> tworzy tekst w formacie Markdown (1) na podstawie przekazanych jej danych, a następnie go zwraca.</p>
<p>Ostatnim krokiem jest dodanie wywołania funkcji <code>createMissingDiscussions()</code> na koniec głównej logiki skryptu:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">await</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createMissingDiscussions</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	owner: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">REPO_OWNER</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	repo: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">REPO_NAME</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	categoryId: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">CATEGORY_ID</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	posts: missingPosts, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	blogUrl: env.</span><span style="color:#005CC5;--shiki-dark:#79B8FF">BLOG_URL</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Do wywołania przekazujemy:</p>
<ol>
<li>właściciela repozytorium,</li>
<li>nazwę repozytorium,</li>
<li>ID kategorii dyskusji,</li>
<li>listę wpisów, dla których chcemy stworzyć dyskusje,</li>
<li>URL bloga, żeby móc stworzyć ładne linki w dyskusjach.</li>
</ol>
<p>Jeśli wszystko poszło dobrze i odpalimy teraz nasz skrypt, powinien wykryć, ile wpisów nie ma swoich dyskusji i zacząć&nbsp;je sukcesywnie dodawać!</p>
<p>Całość kodu, z dodatkową&nbsp;dokumentacją, znaleźć można w <a href="https://github.com/Comandeer/fix-discussions" rel="noreferrer noopener">repozytorium <code>Comandeer/fix-discussions</code></a>.</p>
<p>I to tyle. Zgłoszony błąd powinien zostać naprawiony!</p>
]]></content>
		</entry>
	
		<entry>
			<title type="html">W labiryncie żądań</title>
			
				<author>
					<name>Comandeer</name>
				</author>
			
			<link href="https://blog.comandeer.pl/w-labiryncie-zadan" rel="alternate" type="text/html"/>
			<published>2025-12-02T23:27:00.000Z</published>
			<updated>2025-12-02T23:27:00.000Z</updated>
			<id>https://blog.comandeer.pl/w-labiryncie-zadan</id>
			
				<summary><![CDATA[Krótko o tym, jak przy pomocy URLPatternu zrobić prymitywnego Expressa]]></summary>
			
			<content type="html"><![CDATA[<p>Praktycznie od samego początku istnienia Node’a towarzyszył mu framework <a href="https://expressjs.com/" rel="noreferrer noopener">Express.js</a>, który pozwala tworzyć backend. I ma też jeden ficzer, który dzisiaj jest w światku backendowego JS-a standardem: <a href="https://expressjs.com/en/starter/basic-routing.html" rel="noreferrer noopener">ścieżki (routing)</a>.</p>
<div class="note" role="note" aria-labelledby="note-1"><p class="note__label" id="note-1">Dygresja</p><div class="note__content"><p>Nie twierdzę, że Express.js wynalazł ścieżki. Niemniej zdecydowanie przyczynił się do tego, jak wyglądają w ekosystemie JS-a.</p></div></div>
<h2 id="ścieżki"><a class="header-anchor" href="https://blog.comandeer.pl/w-labiryncie-zadan#ścieżki">Ścieżki</a></h2>
<p>Zerknijmy na <a href="https://expressjs.com/en/starter/hello-world.html" rel="noreferrer noopener">najprostszy przykład użycia Expressa</a>. Jest tam wykorzystana jedna ścieżka:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">req</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">res</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	res.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">send</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Hello World!'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Powyższy kod oznacza, że jeśli ktoś wejdzie na stronę główną, czyli ścieżkę <code>/</code> (1), to dostanie w odpowiedzi tekst <q lang="en">Hello World!</q> (2). Nazwa metody <code>#get()</code> również nie jest przypadkowa, bo odpowiada <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/GET" rel="noreferrer noopener">metodzie HTTP <code>GET</code></a>. Jeśli z kolei chcielibyśmy dodać&nbsp;funkcję, która obsługiwałaby wysyłkę formularza, to możemy użyć <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Methods/POST" rel="noreferrer noopener">metody HTTP <code>POST</code></a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">req</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">res</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	res.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">send</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Hello World!'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Express obsługuje w taki sposób wszystkie najbardziej popularne metody HTTP oraz <a href="https://expressjs.com/en/5x/api.html#routing-methods" rel="noreferrer noopener">sporo mniej popularnych</a>. Dla porządku dodam jeszcze, że parametr <code>req</code> to <a href="https://expressjs.com/en/5x/api.html#req" rel="noreferrer noopener">żądanie wysłane z przeglądarki</a>, a <code>res</code> – <a href="https://expressjs.com/en/5x/api.html#res" rel="noreferrer noopener">odpowiedź, jaką odsyłamy</a>.</p>
<p>Natomiast bohaterem dzisiejszego odcinka są ścieżki. W tym przykładzie mieliśmy do czynienia z najprostszą, <code>/</code>. Niemniej można tworzyć też o wiele bardziej skomplikowane, np.:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/user/:id'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">req</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">res</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	res.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">send</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">`User ${</span><span style="color:#24292E;--shiki-dark:#E1E4E8">req</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">params</span><span style="color:#032F62;--shiki-dark:#9ECBFF">.</span><span style="color:#24292E;--shiki-dark:#E1E4E8">id</span><span style="color:#032F62;--shiki-dark:#9ECBFF">}`</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Ta obsługa żądań odpali się tylko dla adresów pasujących do wzoru <code>/user/:id</code>, a więc np. <code>/user/100</code>, <code>/user/Comandeer</code> itd. Ale już nie <code>/user</code>. Można też&nbsp;część&nbsp;ścieżki oznaczyć jako opcjonalną:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/article/:id{/:title}'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">req</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">res</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    res.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">send</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Super artykuł'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Id artykułu jest wymagane, natomiast jego tytuł – już&nbsp;nie. Stąd ta ścieżka pasuje zarówno do adresu <code>/article/1618/comandeer-jest-sexy</code>, jak i <code>/article/67</code>.</p>
<p>Można też skorzystać&nbsp;z <code>*</code>, żeby zastąpić dowolną liczbę znaków, np <code>/user/*</code> będzie pasować do <code>/user/Comandeer</code>, ale też&nbsp;<code>/user/to/chyba/nie/powinno/tak/dzialac</code>.</p>
<p>Samymi ścieżkami, bez całej reszty Expressa, można się pobawić, instalując <a href="https://www.npmjs.com/package/path-to-regexp" rel="noreferrer noopener">pakiet npm <code>path-to-regexp</code></a>. Liczba ściągnięć&nbsp;dobrze pokazuje, że praktycznie wszyscy z niego korzystają. Stał się na tyle dobrze zakorzenionym <i lang="la">de facto</i> standardem, że… powstał na jego podstawie faktyczny standard.</p>
<h2 id="urlpattern"><a class="header-anchor" href="https://blog.comandeer.pl/w-labiryncie-zadan#urlpattern"><code>URLPattern</code></a></h2>
<p><a href="https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API" rel="noreferrer noopener"><code>URLPattern</code></a> to API <a href="https://urlpattern.spec.whatwg.org/" rel="noreferrer noopener">standaryzowane przez WHATWG</a>. Specyfikacja wprost zaznacza, że <a href="https://urlpattern.spec.whatwg.org/#pattern-strings:~:text=This%20pattern%20syntax%20is%20directly%20based%20on%20the%20syntax%20used%20by%20the%20popular%20path%2Dto%2Dregexp%20JavaScript%20library" rel="noreferrer noopener">składnia wzorów jest oparta na bibliotece <code>path-to-regexp</code></a>. Niemniej samo API jest zdecydowanie bardziej… przeglądarkowe, Przyjrzyjmy się, w jaki sposób przenieść do <code>URLPattern</code>u przykład z artykułem:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> pattern</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> URLPattern</span><span style="color:#24292E;--shiki-dark:#E1E4E8">({ </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">  pathname: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/article/:id{/:title}*'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">});</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( pattern.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">exec</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/article/1618/jakis-tytul'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// null</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( pattern.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">exec</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/article/67'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// null</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku tworzymy nowy <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/URLPattern" rel="noreferrer noopener">obiekt <code>URLPattern</code></a> (1), do którego przekazujemy obiekt z opcjami. Tak naprawdę obiekt ten może mieć wszystkie własności <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL" rel="noreferrer noopener">obiektu <code>URL</code></a> – a zatem nic nie stoi na przeszkodzie, żeby np. stworzyć… walidator <a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes" rel="noreferrer noopener">schematu</a>, odrzucający URL-e bez HTTPS. W naszym przykładzie chcemy jedynie porównać samą&nbsp;<a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Path" rel="noreferrer noopener">ścieżkę URL-a</a> (czyli to, co następuje po domenie), zatem używamy <a href="https://developer.mozilla.org/en-US/docs/Web/API/URLPattern/pathname" rel="noreferrer noopener">własności <code>pathname</code></a> (2). Warto zwrócić uwagę, że składnia standardu jest <em>oparta</em> na bibliotece <code>path-to-regexp</code>, ale nie jest <em>identyczna</em>. W tym wypadku opcjonalną&nbsp;grupę musimy oznaczyć&nbsp;dodatkowo <code>*</code> (jak w wyrażeniach regularnych!). Następnie taki wzorzec testujemy na dwóch ścieżkach i… dostajemy <code>null</code> w obydwu przypadkach.</p>
<p>Dzieje się&nbsp;tak, ponieważ <code>URLPattern</code> działa z URL-ami i wymaga <em>pełnego</em> URL-a, nie relatywnego, jak w naszym przykładzie. Można ten problem rozwiązać na trzy sposoby:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( pattern.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">exec</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'https://blog.comandeer.pl/article/1618'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ) </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( pattern.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">exec</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/article/67'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'https://blog.comandeer.pl'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( pattern.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">exec</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    pathname: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/article/1618/dziala'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} ) );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Najbardziej oczywisty to podanie pełnego URL-a (1). Można też podać samą ścieżkę, a jako drugi argument podać tzw. bazowy URL (2). Wówczas przeglądarka sama rozwiąże pełny adres (czyli po prostu doklei ścieżkę do bazowego URL-a). W końcu można przekazać obiekt z własnością&nbsp;<code>pathname</code> (3) – czyli podobny do tego, który przekazaliśmy do konstruktora. Warto też&nbsp;zauważyć, że w tym przykładzie jest całkowicie nieistotne, jaką&nbsp;domenę dodamy do ścieżki – bo ostatecznie tylko ścieżkę chcemy porównywać.</p>
<p>Konstruktor również przyjmuje te trzy rodzaje opcji:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> URLPattern</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    pathname: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/article/:id{/:title}*'</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// vs</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> URLPattern</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'https://blog.comandeer.pl/article/:id{/:title}*'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"><span style="color:#6A737D;--shiki-dark:#6A737D">// vs</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> URLPattern</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/article/:id{/:title}*'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'https://blog.comandeer.pl'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> );</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W przypadku konstruktora dobrany URL ma jednak spore znaczenie. Podanie pełnego URL-a sprawi, że będzie on używany do dopasowania, więc tylko URL-e z originu <code>https://blog.comandeer.pl</code> będą brane pod uwagę.</p>
<p>Spójrzmy na <a href="https://urlpattern.spec.whatwg.org/#dictdef-urlpatternresult" rel="noreferrer noopener">rezultat metody <code>URLPattern#exec()</code></a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSON</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"hash"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"hostname"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"blog.comandeer.pl"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"blog.comandeer.pl"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"inputs"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#032F62;--shiki-dark:#9ECBFF">		"https://blog.comandeer.pl/article/1618"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	],</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"password"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"pathname"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"id"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"1618"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/article/1618"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"port"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"protocol"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"https"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"search"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"username"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">""</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Wygląda strasznie na pierwszy rzut oka, ale takie nie jest w rzeczywistości! Po raz kolejny mamy tutaj wszystkie własności URL-a. Do tego tablicę <code>inputs</code>, zawierającą tablicę URL-i, na których <code>URLPattern#exec()</code> zostało wywołane. Jeśli któryś z elementów URL-a został dopasowany, to znajduje się w odpowiedniej własności, np. <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/hostname" rel="noreferrer noopener"><code>hostname</code></a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSON</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"hostname"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"0"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"blog.comandeer.pl"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"blog.comandeer.pl"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Z racji tego, że domenę dopasowywaliśmy w całości, we własności <code>groups</code> jest tylko jedna, domyślna grupa <code>0</code>, która zawiera to samo, co własność <code>input</code>. Natomiast we własności <code>pathname</code> robi się już&nbsp;ciekawiej:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSON</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"pathname"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"groups"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">			"id"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"1618"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">		"input"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"/article/1618"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>We własności <code>input</code> wciąż&nbsp;jest pełne dopasowanie, niemniej pojawiła się&nbsp;grupa o nazwie, jaką mieliśmy we wzorze – <code>id</code>; A w niej: poprawnie dopasowany id artykułu!</p>
<p>I tak, porównując to do ścieżek w Expressie, wydaje się to wszystko niepotrzebnie skomplikowane. I <em>prawdopodobnie takie jest</em>. Ale nie można zapominać&nbsp;przy tym, że to musi się dobrze wpisywać w ekosystem przeglądarki, w której URL-e istnieją od zawsze. I każdy nowy element musi pasować do tych już istniejących. Dodatkowo, myślę, że <code>URLPattern</code> można z powodzeniem zaliczyć&nbsp;do tych niskopoziomowych API, które niekoniecznie warto używać bezpośrednio, ale warto na nich budować swoje własne rozwiązania.</p>
<p>No i umyślnie w przypadku <code>URLPattern</code>u używałem terminu “wzorzec” zamiast “ścieżka” (tę zostawiając dla URL-i). Dzięki temu, że <code>URLPattern</code> dopasowuje całe URL-e, można go zastosować do wielu innych rzeczy, niż&nbsp;tylko do ścieżek (jak np. wspomniany wyżej walidator). Zresztą&nbsp;to API <a href="https://github.com/whatwg/urlpattern/blob/main/explainer.md#introduction" rel="noreferrer noopener">oryginalnie powstało na potrzeby Service Workerów</a>, przy okazji rozwiązując też inne problemy.</p>
<h2 id="prymitywny-expressjs"><a class="header-anchor" href="https://blog.comandeer.pl/w-labiryncie-zadan#prymitywny-expressjs">Prymitywny Express.js</a></h2>
<p>Choć&nbsp;<code>URLPattern</code> zaczynał jako API przeglądarkowe, to obecnie jest dostępny także w środowiskach backendowych – w tym w Node.js (od wersji 23). A to oznacza, że można go wykorzystać&nbsp;do stworzenia własnej wersji Express.js!</p>
<p>Na początku stwórzmy plik <code>server.mjs</code>, z którego wyeksportujemy odpowiednik funkcji <code>express()</code> – funkcję <code>server()</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> server</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Funkcja ta zwróci obiekt z API naszego “frameworka”:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> server</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		listen</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Nasz framework będzie miał tylko trzy metody – <code>#get()</code> (1) do obsługi żądań GET, <code>#post()</code> (2) do obsługi żądań POST oraz <code>#listen()</code> (3) do odpalenia serwera na wybranym porcie. Każda z funkcji zwraca <code>this</code> (4, 5, 6), dzięki czemu można tworzyć łańcuchy wywołań.</p>
<p>Żeby jednak móc odpalić serwer, trzeba najpierw go mieć. Tutaj na ratunek przychodzi wbudowany w Node <a href="https://nodejs.org/api/http.html" rel="noreferrer noopener">moduł <code>node:http</code></a>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { createServer } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> 'node:http'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> server</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> server</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createServer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		listen</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">port</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> 3000</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			server.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">listen</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( port ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">};</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku importujemy funkcję <code>createServer()</code> z modułu <code>node:http</code> (1). Następnie tworzymy przy jej pomocy nowy serwer (2), a następnie w metodzie <code>#listen()</code> odpalamy go na podanym porcie (3).</p>
<p>Niemniej serwer, który nic nie robi, jest mało użyteczny. Dodajmy zatem obsługę&nbsp;żądań:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> server</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> server</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createServer</span><span style="color:#24292E;--shiki-dark:#E1E4E8">();</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> handlers</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> []; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	server.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">on</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'request'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">request</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">response</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		for</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> handler</span><span style="color:#D73A49;--shiki-dark:#F97583"> of</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> handlers ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">handler</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( request, response ) ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">				return</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		response.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">writeHead</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">404</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		response.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">end</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">    […]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Do naszego serwera dodajemy obsługę <a href="https://nodejs.org/api/http.html#event-request" rel="noreferrer noopener">zdarzenia <code>request</code></a> (1).  Nasłuchiwacz dostaje dwa argumenty: żądanie (<code>request</code>) oraz odpowiedź (<code>response</code>). Zatem bardzo podobnie do tego, jak działa to w Express.js. Iterujemy po wszystkich handlerach (2) w tablicy <code>handlers</code> (3) i wywołujemy każdy, przekazując do niego żądanie i odpowiedź (4). Jeśli handler zwróci <code>true</code>, kończymy całą&nbsp;obsługę żądania (5). Natomiast jeśli obsługa nie zostanie zakończona w tym miejscu, to znak, że żaden handler nie obsłużył żądania (nie zwrócił&nbsp;<code>true</code>). Dlatego też ustawiamy <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/404" rel="noreferrer noopener">status 404</a> przy pomocy <a href="https://nodejs.org/api/http.html#responsewriteheadstatuscode-statusmessage-headers" rel="noreferrer noopener">metody <code>response#writeHead()</code></a> (6), a następnie wysyłamy pustą odpowiedź przy pomocy <a href="https://nodejs.org/api/http.html#responseenddata-encoding-callback" rel="noreferrer noopener">metody <code>response#end()</code></a> (7).</p>
<p>W tym momencie serwer dla dowolnego żądania zwróci błąd 404. Nie jest to mocno użyteczne, więc wypada dodać dodawanie handlerów:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">export</span><span style="color:#D73A49;--shiki-dark:#F97583"> function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> server</span><span style="color:#24292E;--shiki-dark:#E1E4E8">() {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	[…]</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">handler</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> internalHandler</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createHandler</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'GET'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, path, handler ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			handlers.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( internalHandler ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		},</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		post</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">handler</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> internalHandler</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createHandler</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'POST'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, path, handler ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			handlers.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">push</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( internalHandler ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> this</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		[…]</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>W metodach <code>#get()</code> i <code>#post()</code> pojawiły się&nbsp;wywołania funkcji <code>createHandler()</code> (1, 2). Tak powstałe handlery są następnie dodawane do tablicy <code>handlers</code> (3, 4).</p>
<p>Sama funkcja <code>createHandler()</code> wygląda z kolei tak:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> createHandler</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">method</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">path</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">handler</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> pattern</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#D73A49;--shiki-dark:#F97583"> new</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> URLPattern</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		pathname: path </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">request</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">response</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 4</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( request.method </span><span style="color:#D73A49;--shiki-dark:#F97583">!==</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> method ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> matchedUrl</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> pattern.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">exec</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( request.url ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		if</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ( matchedUrl </span><span style="color:#D73A49;--shiki-dark:#F97583">===</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> null</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">			return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> false</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 9</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		handler</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#6F42C1;--shiki-dark:#B392F0">toHandlerRequest</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( request. matchedUrl ), </span><span style="color:#6F42C1;--shiki-dark:#B392F0">toHandlerResponse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( response ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 10</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">		return</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="color:#24292E;--shiki-dark:#E1E4E8">;</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> toHandlerRequest</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">request</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">matchedUrl</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		url: request.url, </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 11</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		params: matchedUrl.pathname.groups </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 12</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">function</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> toHandlerResponse</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">response</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) {</span></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">	return</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> {</span></span>
<span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">		send</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#E36209;--shiki-dark:#FFAB70">content</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 13</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			response.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">writeHead</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">200</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, { </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Content-Type'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'application/json'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> } ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 14</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">			response.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">end</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">JSON</span><span style="color:#24292E;--shiki-dark:#E1E4E8">.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">stringify</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( content ) ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 15</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		}</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	};</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Funkcja przyjmuje trzy parametry (1):</p>
<ol>
<li><code>method</code> – nazwę metody HTTP (w tym przypadku <code>GET</code> albo <code>POST</code>),</li>
<li><code>path</code> – ścieżkę,</li>
<li><code>handler</code> – funkcję do obsługi żądania.</li>
</ol>
<p>Następnie tworzymy nowy <code>URLPattern</code> (2) i przekazujemy do niego naszą ścieżkę (3). Potem tworzymy funkcję, którą na koniec zwrócimy (4). Przyjmuje ona dwa parametry: żądanie (<code>request</code>) oraz odpowiedź (<code>response</code>). Wewnątrz tej funkcji na samym początku sprawdzamy, czy żądanie przyszło przy pomocy interesującej nas metody (5). Jeśli nie, kończymy obsługę tego żądania i zwracamy <code>false</code> (6). Jeśli tak, dopasowujemy URL żądania do naszego wzorca (7). Jeśli nie udało się dopasować (8), kończymy obsługę&nbsp;żądania i zwracamy <code>false</code> (9). Dzięki tym dwóm sprawdzeniom jesteśmy w stanie poprawnie obsługiwać błędy 404 w głównym nasłuchiwaczu serwera. Jeśli żądanie zostało dopasowane, wywołujemy przekazany <code>handler()</code> (10) wraz ze specjalnie spreparowanymi żądaniem i odpowiedzią.</p>
<p>Żądanie, jakie dostanie <code>handler</code>, ma tak naprawdę tylko dwie własności – oryginalny URL (11) jako <code>url</code> oraz dopasowane grupy (12) jako <code>params</code>. Z kolei odpowiedź ma tylko jedną metodę, <code>#send()</code> (13), która przyjmuje jeden parametr – zawartość odpowiedzi (<code>content</code>). Ta metoda ustawia <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/200" rel="noreferrer noopener">status 200</a> oraz <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Type" rel="noreferrer noopener">nagłówek <code>Content-Type</code></a> na <code>application/json</code> , sygnalizując, że odpowiedź będzie JSON-em (14). Następnie <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify" rel="noreferrer noopener">serializuje</a> <code>content</code> i wysyła tak powstały ciąg znaków do przeglądarki (15).</p>
<p>Pora przetestować nasz serwer! Stwórzmy w tym celu plik <code>app.mjs</code>:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JavaScript</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">import</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { server } </span><span style="color:#D73A49;--shiki-dark:#F97583">from</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> './server.mjs'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">; </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 1</span></span>
<span class="line"></span>
<span class="line"><span style="color:#D73A49;--shiki-dark:#F97583">const</span><span style="color:#005CC5;--shiki-dark:#79B8FF"> app</span><span style="color:#D73A49;--shiki-dark:#F97583"> =</span><span style="color:#6F42C1;--shiki-dark:#B392F0"> server</span><span style="color:#24292E;--shiki-dark:#E1E4E8">(); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 2</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">request</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">response</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 3</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	response.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">send</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		version: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'1.0.0'</span><span style="color:#6A737D;--shiki-dark:#6A737D"> // 4</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">get</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'/user/:id'</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, ( </span><span style="color:#E36209;--shiki-dark:#FFAB70">request</span><span style="color:#24292E;--shiki-dark:#E1E4E8">, </span><span style="color:#E36209;--shiki-dark:#FFAB70">response</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ) </span><span style="color:#D73A49;--shiki-dark:#F97583">=&gt;</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> { </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 5</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	response.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">send</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( {</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">		id: request.params.id </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 6</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">	} );</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">} );</span></span>
<span class="line"></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">app.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">listen</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#005CC5;--shiki-dark:#79B8FF">3000</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 7</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">console.</span><span style="color:#6F42C1;--shiki-dark:#B392F0">log</span><span style="color:#24292E;--shiki-dark:#E1E4E8">( </span><span style="color:#032F62;--shiki-dark:#9ECBFF">'Server is listening on port 3000'</span><span style="color:#24292E;--shiki-dark:#E1E4E8"> ); </span><span style="color:#6A737D;--shiki-dark:#6A737D">// 8</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Na początku importujemy funkcję&nbsp;<code>server()</code> z pliku <code>./server.mjs</code> (1). Następnie tworzymy nowy serwer i przypisujemy go do zmiennej <code>app</code> (2). Potem dodajemy obsługę żądań do strony głównej, <code>/</code> (3). W odpowiedzi wysyłamy obiekt z wersją naszego “API” (4). Potem dodajemy obsługę dla ścieżki <code>/user/:id</code> (5). Żeby przetestować, czy ich obsługa działa poprawnie, zwracamy przekazany parametr <code>id</code> (6). Na sam koniec odpalamy serwer na porcie 3000 (7) oraz wyświetlamy w konsoli informację, że serwer działa (8).</p>
<p>Teraz można odpalić serwer:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">Shell</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#6F42C1;--shiki-dark:#B392F0">node</span><span style="color:#032F62;--shiki-dark:#9ECBFF"> ./app.mjs</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>A następnie przejść do przeglądarki i sprawdzić, czy dostaniemy poprawną&nbsp;odpowiedź. Jeśli wejdziemy pod adres <code>http://localhost:3000</code>, to powinniśmy otrzymać:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSON</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"version"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"1.0.0"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Natomiast jeśli wejdziemy pod adres <code>http://localhost:3000/user/Comandeer</code>, powinniśmy otrzymać:</p>
<figure class="code" typeof="SoftwareSourceCode">
			<figcaption class="code__caption">
				<span class="code__title" property="programmingLanguage">JSON</span>
				
			</figcaption>
			<div class="code__code" translate="no" property="text">
				<pre class="shiki shiki-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">{</span></span>
<span class="line"><span style="color:#005CC5;--shiki-dark:#79B8FF">	"id"</span><span style="color:#24292E;--shiki-dark:#E1E4E8">: </span><span style="color:#032F62;--shiki-dark:#9ECBFF">"Comandeer"</span></span>
<span class="line"><span style="color:#24292E;--shiki-dark:#E1E4E8">}</span></span>
<span class="line"></span></code></pre>

			</div>
		</figure><p>Jeśli wpiszemy dowolny nieobsługiwany adres (np. <code>http://localhost:3000/whatever</code>), przeglądarka powinna wyświetlić&nbsp;domyślny błąd 404.</p>
<div class="note" role="note" aria-labelledby="note-2"><p class="note__label" id="note-2">Dygresja</p><div class="note__content"><p>Oczywiście ten prymitywny serwer <strong>NIE NADAJE SIĘ DO CELÓW PRODUKCYJNCH</strong>. Powstał jedynie po to, by pokazać, jak można wykorzystać <code>URLPattern</code> w praktyce.</p></div></div>
<p>Całość kodu, wraz z dokumentacją w formacie <a href="https://en.wikipedia.org/wiki/JSDoc" rel="noreferrer noopener">JSDoc</a>, znajduje się w <a href="https://gist.github.com/Comandeer/1588f257b94182f02989c7f36ca7fccf" rel="noreferrer noopener">Giście</a>.</p>
]]></content>
		</entry>
	
</feed>
