Skip to content

Commit be791e7

Browse files
committed
deploy: b8bf5ca
1 parent 276c1e7 commit be791e7

32 files changed

+457
-75
lines changed

Diff for: 404.html

+3-5
Large diffs are not rendered by default.

Diff for: authors/index.html

+3-5
Large diffs are not rendered by default.

Diff for: categories/index.html

+3-5
Large diffs are not rendered by default.

Diff for: images/avatars/daniel-rozycki.jpeg

220 KB
Loading

Diff for: images/avatars/kamil-kucharski.png

36.9 KB
Loading

Diff for: images/avatars/lukasz-langa.png

226 KB
Loading

Diff for: images/avatars/michal-moroz.jpg

330 KB
Loading

Diff for: images/avatars/sebastian-buczynski.png

1.9 MB
Loading

Diff for: images/sponsors/indiebi.png

13.5 KB
Loading

Diff for: images/sponsors/sunscrapers.png

11.9 KB
Loading

Diff for: index.html

+5-6
Large diffs are not rendered by default.

Diff for: index.json

+1-1
Large diffs are not rendered by default.

Diff for: index.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Python Łódź</title><link>https://pythonlodz.org/</link><description>Recent content on Python Łódź</description><generator>Hugo -- gohugo.io</generator><language>pl</language><copyright>© 2025 Python Łódź</copyright><lastBuildDate>Wed, 01 Jan 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://pythonlodz.org/index.xml" rel="self" type="application/rss+xml"/><item><title>Informacja o przetwarzaniu danych osobowych prelegenta spotkań Python Łódź</title><link>https://pythonlodz.org/prelegenci-przetwarzanie-danych/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/prelegenci-przetwarzanie-danych/</guid><description/></item><item><title>Polityka Cookies</title><link>https://pythonlodz.org/polityka-cookies/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/polityka-cookies/</guid><description/></item><item><title>Regulamin uczestnictwa w spotkaniach Python Łódź</title><link>https://pythonlodz.org/regulamin-uczestnictwa/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/regulamin-uczestnictwa/</guid><description/></item></channel></rss>
1+
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Python Łódź</title><link>https://pythonlodz.org/</link><description>Recent content on Python Łódź</description><generator>Hugo -- gohugo.io</generator><language>pl</language><copyright>© 2025 Python Łódź</copyright><lastBuildDate>Wed, 26 Mar 2025 00:00:00 +0000</lastBuildDate><atom:link href="https://pythonlodz.org/index.xml" rel="self" type="application/rss+xml"/><item><title>Meetup #57</title><link>https://pythonlodz.org/spotkania/57/</link><pubDate>Wed, 26 Mar 2025 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/spotkania/57/</guid><description/></item><item><title>Meetup #56</title><link>https://pythonlodz.org/spotkania/56/</link><pubDate>Wed, 29 Jan 2025 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/spotkania/56/</guid><description/></item><item><title>Informacja o przetwarzaniu danych osobowych prelegenta spotkań Python Łódź</title><link>https://pythonlodz.org/prelegenci-przetwarzanie-danych/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/prelegenci-przetwarzanie-danych/</guid><description/></item><item><title>Polityka Cookies</title><link>https://pythonlodz.org/polityka-cookies/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/polityka-cookies/</guid><description/></item><item><title>Regulamin uczestnictwa w spotkaniach Python Łódź</title><link>https://pythonlodz.org/regulamin-uczestnictwa/</link><pubDate>Wed, 01 Jan 2025 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/regulamin-uczestnictwa/</guid><description/></item><item><title>Meetup #55</title><link>https://pythonlodz.org/spotkania/55/</link><pubDate>Wed, 27 Nov 2024 00:00:00 +0000</pubDate><guid>https://pythonlodz.org/spotkania/55/</guid><description/></item></channel></rss>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
/**
2+
* A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint.
3+
*
4+
* Thx to these as the inspiration
5+
* https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
6+
* https://autoplay-youtube-player.glitch.me/
7+
*
8+
* Once built it, I also found these:
9+
* https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍)
10+
* https://github.com/Daugilas/lazyYT
11+
* https://github.com/vb/lazyframe
12+
*/
13+
class LiteYTEmbed extends HTMLElement {
14+
connectedCallback() {
15+
this.videoId = this.getAttribute('videoid');
16+
17+
let playBtnEl = this.querySelector('.lty-playbtn');
18+
// A label for the button takes priority over a [playlabel] attribute on the custom-element
19+
this.playLabel = (playBtnEl && playBtnEl.textContent.trim()) || this.getAttribute('playlabel') || 'Play';
20+
21+
this.dataset.title = this.getAttribute('title') || "";
22+
23+
/**
24+
* Lo, the youtube poster image! (aka the thumbnail, image placeholder, etc)
25+
*
26+
* See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md
27+
*/
28+
if (!this.style.backgroundImage) {
29+
this.style.backgroundImage = `url("https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg")`;
30+
this.upgradePosterImage();
31+
}
32+
33+
// Set up play button, and its visually hidden label
34+
if (!playBtnEl) {
35+
playBtnEl = document.createElement('button');
36+
playBtnEl.type = 'button';
37+
playBtnEl.classList.add('lty-playbtn');
38+
this.append(playBtnEl);
39+
}
40+
if (!playBtnEl.textContent) {
41+
const playBtnLabelEl = document.createElement('span');
42+
playBtnLabelEl.className = 'lyt-visually-hidden';
43+
playBtnLabelEl.textContent = this.playLabel;
44+
playBtnEl.append(playBtnLabelEl);
45+
}
46+
47+
this.addNoscriptIframe();
48+
49+
// for the PE pattern, change anchor's semantics to button
50+
if(playBtnEl.nodeName === 'A'){
51+
playBtnEl.removeAttribute('href');
52+
playBtnEl.setAttribute('tabindex', '0');
53+
playBtnEl.setAttribute('role', 'button');
54+
// fake button needs keyboard help
55+
playBtnEl.addEventListener('keydown', e => {
56+
if( e.key === 'Enter' || e.key === ' ' ){
57+
e.preventDefault();
58+
this.activate();
59+
}
60+
});
61+
}
62+
63+
// On hover (or tap), warm up the TCP connections we're (likely) about to use.
64+
this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {once: true});
65+
this.addEventListener('focusin', LiteYTEmbed.warmConnections, {once: true});
66+
67+
// Once the user clicks, add the real iframe and drop our play button
68+
// TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time
69+
// We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003
70+
this.addEventListener('click', this.activate);
71+
72+
// Chrome & Edge desktop have no problem with the basic YouTube Embed with ?autoplay=1
73+
// However Safari desktop and most/all mobile browsers do not successfully track the user gesture of clicking through the creation/loading of the iframe,
74+
// so they don't autoplay automatically. Instead we must load an additional 2 sequential JS files (1KB + 165KB) (un-br) for the YT Player API
75+
// TODO: Try loading the the YT API in parallel with our iframe and then attaching/playing it. #82
76+
this.needsYTApi = this.hasAttribute("js-api") || navigator.vendor.includes('Apple') || navigator.userAgent.includes('Mobi');
77+
}
78+
79+
/**
80+
* Add a <link rel={preload | preconnect} ...> to the head
81+
*/
82+
static addPrefetch(kind, url, as) {
83+
const linkEl = document.createElement('link');
84+
linkEl.rel = kind;
85+
linkEl.href = url;
86+
if (as) {
87+
linkEl.as = as;
88+
}
89+
document.head.append(linkEl);
90+
}
91+
92+
/**
93+
* Begin pre-connecting to warm up the iframe load
94+
* Since the embed's network requests load within its iframe,
95+
* preload/prefetch'ing them outside the iframe will only cause double-downloads.
96+
* So, the best we can do is warm up a few connections to origins that are in the critical path.
97+
*
98+
* Maybe `<link rel=preload as=document>` would work, but it's unsupported: http://crbug.com/593267
99+
* But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity.
100+
*/
101+
static warmConnections() {
102+
if (LiteYTEmbed.preconnected) return;
103+
104+
// The iframe document and most of its subresources come right off youtube.com
105+
LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com');
106+
// The botguard script is fetched off from google.com
107+
LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
108+
109+
// Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling.
110+
LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
111+
LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
112+
113+
LiteYTEmbed.preconnected = true;
114+
}
115+
116+
fetchYTPlayerApi() {
117+
if (window.YT || (window.YT && window.YT.Player)) return;
118+
119+
this.ytApiPromise = new Promise((res, rej) => {
120+
var el = document.createElement('script');
121+
el.src = 'https://www.youtube.com/iframe_api';
122+
el.async = true;
123+
el.onload = _ => {
124+
YT.ready(res);
125+
};
126+
el.onerror = rej;
127+
this.append(el);
128+
});
129+
}
130+
131+
/** Return the YT Player API instance. (Public L-YT-E API) */
132+
async getYTPlayer() {
133+
if(!this.playerPromise) {
134+
await this.activate();
135+
}
136+
137+
return this.playerPromise;
138+
}
139+
140+
async addYTPlayerIframe() {
141+
this.fetchYTPlayerApi();
142+
await this.ytApiPromise;
143+
144+
const videoPlaceholderEl = document.createElement('div')
145+
this.append(videoPlaceholderEl);
146+
147+
const paramsObj = Object.fromEntries(this.getParams().entries());
148+
149+
this.playerPromise = new Promise(resolve => {
150+
let player = new YT.Player(videoPlaceholderEl, {
151+
width: '100%',
152+
videoId: this.videoId,
153+
playerVars: paramsObj,
154+
events: {
155+
'onReady': event => {
156+
event.target.playVideo();
157+
resolve(player);
158+
}
159+
}
160+
});
161+
});
162+
}
163+
164+
// Add the iframe within <noscript> for indexability discoverability. See https://github.com/paulirish/lite-youtube-embed/issues/105
165+
addNoscriptIframe() {
166+
const iframeEl = this.createBasicIframe();
167+
const noscriptEl = document.createElement('noscript');
168+
// Appending into noscript isn't equivalant for mysterious reasons: https://html.spec.whatwg.org/multipage/scripting.html#the-noscript-element
169+
noscriptEl.innerHTML = iframeEl.outerHTML;
170+
this.append(noscriptEl);
171+
}
172+
173+
getParams() {
174+
const params = new URLSearchParams(this.getAttribute('params') || []);
175+
params.append('autoplay', '1');
176+
params.append('playsinline', '1');
177+
return params;
178+
}
179+
180+
async activate(){
181+
if (this.classList.contains('lyt-activated')) return;
182+
this.classList.add('lyt-activated');
183+
184+
if (this.needsYTApi) {
185+
return this.addYTPlayerIframe(this.getParams());
186+
}
187+
188+
const iframeEl = this.createBasicIframe();
189+
this.append(iframeEl);
190+
191+
// Set focus for a11y
192+
iframeEl.focus();
193+
}
194+
195+
createBasicIframe(){
196+
const iframeEl = document.createElement('iframe');
197+
iframeEl.width = 560;
198+
iframeEl.height = 315;
199+
// No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include
200+
iframeEl.title = this.playLabel;
201+
iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
202+
iframeEl.allowFullscreen = true;
203+
// AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL
204+
// https://stackoverflow.com/q/64959723/89484
205+
iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}`;
206+
return iframeEl;
207+
}
208+
209+
/**
210+
* In the spirit of the `lowsrc` attribute and progressive JPEGs, we'll upgrade the reliable
211+
* poster image to a higher resolution one, if it's available.
212+
* Interestingly this sddefault webp is often smaller in filesize, but we will still attempt it second
213+
* because getting _an_ image in front of the user if our first priority.
214+
*
215+
* See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md for more details
216+
*/
217+
upgradePosterImage() {
218+
// Defer to reduce network contention.
219+
setTimeout(() => {
220+
const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`;
221+
const img = new Image();
222+
img.fetchPriority = 'low'; // low priority to reduce network contention
223+
img.referrerpolicy = 'origin'; // Not 100% sure it's needed, but https://github.com/ampproject/amphtml/pull/3940
224+
img.src = webpUrl;
225+
img.onload = e => {
226+
// A pretty ugly hack since onerror won't fire on YouTube image 404. This is (probably) due to
227+
// Youtube's style of returning data even with a 404 status. That data is a 120x90 placeholder image.
228+
// … per "annoying yt 404 behavior" in the .md
229+
const noAvailablePoster = e.target.naturalHeight == 90 && e.target.naturalWidth == 120;
230+
if (noAvailablePoster) return;
231+
232+
this.style.backgroundImage = `url("${webpUrl}")`;
233+
}
234+
}, 100);
235+
}
236+
}
237+
// Register custom element
238+
customElements.define('lite-youtube', LiteYTEmbed);

Diff for: lib/lite-youtube-embed/lite-yt-embed.css

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
lite-youtube {
2+
background-color: #000;
3+
position: relative;
4+
display: block;
5+
contain: content;
6+
background-position: center center;
7+
background-size: cover;
8+
cursor: pointer;
9+
max-width: 720px;
10+
}
11+
12+
/* gradient */
13+
lite-youtube::before {
14+
content: attr(data-title);
15+
display: block;
16+
position: absolute;
17+
top: 0;
18+
/* Pixel-perfect port of YT's gradient PNG, using https://github.com/bluesmoon/pngtocss plus optimizations */
19+
background-image: linear-gradient(180deg, rgb(0 0 0 / 67%) 0%, rgb(0 0 0 / 54%) 14%, rgb(0 0 0 / 15%) 54%, rgb(0 0 0 / 5%) 72%, rgb(0 0 0 / 0%) 94%);
20+
height: 99px;
21+
width: 100%;
22+
font-family: "YouTube Noto",Roboto,Arial,Helvetica,sans-serif;
23+
color: hsl(0deg 0% 93.33%);
24+
text-shadow: 0 0 2px rgba(0,0,0,.5);
25+
font-size: 18px;
26+
padding: 25px 20px;
27+
overflow: hidden;
28+
white-space: nowrap;
29+
text-overflow: ellipsis;
30+
box-sizing: border-box;
31+
}
32+
33+
lite-youtube:hover::before {
34+
color: white;
35+
}
36+
37+
/* responsive iframe with a 16:9 aspect ratio
38+
thanks https://css-tricks.com/responsive-iframes/
39+
*/
40+
lite-youtube::after {
41+
content: "";
42+
display: block;
43+
padding-bottom: calc(100% / (16 / 9));
44+
}
45+
lite-youtube > iframe {
46+
width: 100%;
47+
height: 100%;
48+
position: absolute;
49+
top: 0;
50+
left: 0;
51+
border: 0;
52+
}
53+
54+
/* play button */
55+
lite-youtube > .lty-playbtn {
56+
display: block;
57+
/* Make the button element cover the whole area for a large hover/click target… */
58+
width: 100%;
59+
height: 100%;
60+
/* …but visually it's still the same size */
61+
background: no-repeat center/68px 48px;
62+
/* YT's actual play button svg */
63+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 48"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="red"/><path d="M45 24 27 14v20" fill="white"/></svg>');
64+
position: absolute;
65+
cursor: pointer;
66+
z-index: 1;
67+
filter: grayscale(100%);
68+
transition: filter .1s cubic-bezier(0, 0, 0.2, 1);
69+
border: 0;
70+
}
71+
72+
lite-youtube:hover > .lty-playbtn,
73+
lite-youtube .lty-playbtn:focus {
74+
filter: none;
75+
}
76+
77+
/* Post-click styles */
78+
lite-youtube.lyt-activated {
79+
cursor: unset;
80+
}
81+
lite-youtube.lyt-activated::before,
82+
lite-youtube.lyt-activated > .lty-playbtn {
83+
opacity: 0;
84+
pointer-events: none;
85+
}
86+
87+
.lyt-visually-hidden {
88+
clip: rect(0 0 0 0);
89+
clip-path: inset(50%);
90+
height: 1px;
91+
overflow: hidden;
92+
position: absolute;
93+
white-space: nowrap;
94+
width: 1px;
95+
}

0 commit comments

Comments
 (0)