<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Todd Schiller - web browsers</title><link href="https://toddschiller.com/" rel="alternate"></link><link href="https://toddschiller.com/feeds/tag/web-browsers.atom.xml" rel="self"></link><id>https://toddschiller.com/</id><updated>2021-03-04T00:00:00-05:00</updated><subtitle>Human ✘ Artificial Intelligence</subtitle><entry><title>A brief history of browser extensibility</title><link href="https://toddschiller.com/blog/history-browser-extensibility.html" rel="alternate"></link><published>2021-03-04T00:00:00-05:00</published><updated>2021-03-04T00:00:00-05:00</updated><author><name>Todd Schiller</name></author><id>tag:toddschiller.com,2021-03-04:/blog/history-browser-extensibility.html</id><summary type="html">&lt;p&gt;Web browsers are the modern operating system.
Google's &lt;a href="https://en.wikipedia.org/wiki/Chrome_OS"&gt;Chrome OS&lt;/a&gt;, an operating
system that runs the Chrome Browser
exclusively, &lt;a href="https://arstechnica.com/gadgets/2021/02/the-worlds-second-most-popular-desktop-operating-system-isnt-macos-anymore/"&gt;is now the second most popular desktop operating system&lt;/a&gt;.
Additionally, the rise of &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt; and similar
cross-platform frameworks means that many of the applications we use every day
(e.g., Slack …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Web browsers are the modern operating system.
Google's &lt;a href="https://en.wikipedia.org/wiki/Chrome_OS"&gt;Chrome OS&lt;/a&gt;, an operating
system that runs the Chrome Browser
exclusively, &lt;a href="https://arstechnica.com/gadgets/2021/02/the-worlds-second-most-popular-desktop-operating-system-isnt-macos-anymore/"&gt;is now the second most popular desktop operating system&lt;/a&gt;.
Additionally, the rise of &lt;a href="https://www.electronjs.org/"&gt;Electron&lt;/a&gt; and similar
cross-platform frameworks means that many of the applications we use every day
(e.g., Slack) are actually browser applications in disguise.&lt;/p&gt;
&lt;p&gt;The ubiquity of browsers has cemented their critical role in consumer
productivity and self-efficacy. Given this importance, I wanted to learn how
browser extensibility has evolved over the years. Since I couldn't
find &lt;a href="https://en.wikipedia.org/wiki/Browser_extension"&gt;a complete history&lt;/a&gt;, I
set out to write my own.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Disclaimer #1: I'm going to gloss over many details to keep this post as
non-technical as possible. I won't, for example, dig into the distinction
between a browser and a rendering engine.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Disclaimer #2: If I make any mistakes or omit anything you feel is important,
please post in the comments. As I have not personally had much exposure to
browsers in non-English-speaking countries, there will undoubtedly be gaps in
this post.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Consumer Web Browsers (1993-)&lt;/h2&gt;
&lt;p&gt;In the beginning, there was Tim Berners-Lee's WorldWideWeb browser (1990).
However, Marc Andreessen's Mosaic (1993) was the first browser to gain
mainstream popularity, as it
was &lt;a href="https://www.w3.org/People/Berners-Lee/FAQ.html#browser"&gt;easy to install, displayed graphics, and provided customer support&lt;/a&gt;.
Andreessen then went on to launch Netscape, which would battle Internet
Explorer (first released in 1995) for dominance in the browser wars of the
late '90s. The browser wars culminated with Microsoft
facing &lt;a href="https://en.wikipedia.org/wiki/United_States_v._Microsoft_Corp."&gt;antitrust action over its integration of Internet Explorer and Windows&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="/assets/images/browser-extensibility/mosaic-mac.webp" alt="Mosaic for Mac" loading="lazy" decoding="async" /&gt;&lt;/p&gt;
&lt;h2&gt;Plug-ins (1995-2015ish)&lt;/h2&gt;
&lt;p&gt;The early web only supported static content. Plug-ins -- native executables that
render dynamic content in the browser -- opened the door for multimedia,
enterprise applications, and gaming. Internet Explorer supported plug-ins via
its &lt;a href="https://en.wikipedia.org/wiki/ActiveX"&gt;ActiveX&lt;/a&gt; technology,
while &lt;a href="https://en.wikipedia.org/wiki/NPAPI"&gt;other browsers supported NPAPI&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;On the consumer side, you basically couldn't browse the web without installing
the Flash (1996) and Java (1998) plug-ins.&lt;/p&gt;
&lt;p&gt;&lt;img src="/assets/images/browser-extensibility/flash-player.webp" alt="Flash Player website in March 2006 via the Internet Archive" loading="lazy" decoding="async" /&gt;&lt;/p&gt;
&lt;p&gt;As native executables, plug-ins were a chronic source of security
vulnerabilities and instability. Despite these problems, it wasn't until the
adoption of the HTML5 standard and modern Web APIs in 2014 that browsers could
rid themselves of plug-ins without sacrificing functionality.&lt;/p&gt;
&lt;p&gt;Desktop browsers began dropping support for plug-ins in 2015,
with &lt;a href="https://en.wikipedia.org/wiki/NPAPI#Support/deprecation"&gt;Chrome leading the charge&lt;/a&gt;.
On mobile, iOS (2003)
famously &lt;a href="https://web.archive.org/web/20200430094807/https://www.apple.com/hotnews/thoughts-on-flash/"&gt;did not offer Flash from the outset&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The Flash plug-in has been the longest holdout due to the ubiquity of Flash
content. Firefox, for instance, refused to drop support for Flash until the
beginning of this year.&lt;/p&gt;
&lt;h2&gt;User Style Sheets (1998-2019ish)&lt;/h2&gt;
&lt;p&gt;With multiple browsers on the market and growing incompatibilities in site
rendering, Tim Berners-Lee founded the World Wide Web Consortium (W3C) in 1994
to develop cross-browser standards.&lt;/p&gt;
&lt;p&gt;One of these standards was 1996's Cascading Style Sheets (CSS) specification.
CSS defines support for &amp;quot;stylesheets&amp;quot; that tell a browser how to style the
content on a page. Stylesheets can come from three different sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Document author: The author of the website&lt;/li&gt;
&lt;li&gt;User: The user of the browser&lt;/li&gt;
&lt;li&gt;User agent: The default style for the user agent (i.e., the browser)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="/assets/images/browser-extensibility/css1-test-suite.webp" alt="Part of the CSS 1 test suite" loading="lazy" decoding="async" /&gt;&lt;/p&gt;
&lt;p&gt;In 1998, Opera 3.5 was the first browser to support CSS and
included &lt;a href="https://dbaron.org/css/user/"&gt;support for user stylesheets&lt;/a&gt;. Internet
Explorer followed suit with the release of Internet Explorer 4 in 1999.&lt;/p&gt;
&lt;p&gt;In 2005, with the introduction of Jason
Barnabe's &lt;a href="https://en.wikipedia.org/wiki/Stylish"&gt;Stylish extension&lt;/a&gt;, consumer
styling began shifting to style manager browser extensions. Compared to user
stylesheets, style managers offer more flexible controls, sharing features, and
ease of use.&lt;/p&gt;
&lt;p&gt;Chrome dropped support for user stylesheets in 2014. Firefox still has support
via the &lt;code&gt;userContent.css&lt;/code&gt;
stylesheet,
&lt;a href="https://en.wikipedia.org/wiki/Stylish"&gt;but the user stylesheet hasn't been accessible from the main interface since 2019&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;**Correction 6/5/2021: Safari still supports a user stylesheet from the Advanced
tab in preferences. Here's
a &lt;a href="http://theoveranalyzed.net/2018/3/16/safaris-custom-style-sheet"&gt;history of custom stylesheets in Safari&lt;/a&gt;.
**&lt;/p&gt;
&lt;h2&gt;Bookmarklets (1998-)&lt;/h2&gt;
&lt;p&gt;Brendan Eich and
Netscape &lt;a href="https://en.wikipedia.org/wiki/JavaScript#Creation_at_Netscape"&gt;introduced JavaScript to the world in 1995&lt;/a&gt;.
JavaScript is a programming language that website authors can include to create
dynamic content and interactive user interfaces.&lt;/p&gt;
&lt;p&gt;JavaScript also opened up a new avenue for browser customization:
bookmarklets. &lt;a href="https://en.wikipedia.org/wiki/Bookmarklet"&gt;Bookmarklets&lt;/a&gt; are
small snippets of JavaScript contained in a bookmark that starts with
&lt;code&gt;javascript:&lt;/code&gt;. When you click the bookmark, it runs the JavaScript in the
context of the page, possibly loading more JavaScript from another source.&lt;/p&gt;
&lt;p&gt;For example, here's a bookmarklet that opens the historical versions of a page
in the Internet Archive's &lt;a href="https://archive.org/web/"&gt;Wayback Machine&lt;/a&gt;:&lt;/p&gt;
&lt;!-- markdownlint-disable MD013 --&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nx"&gt;javascript&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="ow"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://web.archive.org/web/*/&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\/$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- markdownlint-enable MD013 --&gt;
&lt;p&gt;Bookmarklets hit a road bump in 2004 with the introduction of W3C's Content
Security Policy (CSP) specification. Designed to protect sites against malicious
user content and third party code, CSP enabled site owners to lock-down code
origins and network requests.&lt;/p&gt;
&lt;p&gt;However, wanting to maintain extensibility,
the &lt;a href="https://www.w3.org/TR/CSP3/#extensions"&gt;CSP Specification&lt;/a&gt; specifies that
bookmarklets and other extensibility features should be exempt from CSP
restrictions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Policy enforced on a resource SHOULD NOT interfere with the operation of
user-agent features like addons, extensions, or bookmarklets. These kinds of
features generally advance the user's priority over page authors, as espoused
in [HTML-DESIGN].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In practice, things are different. In
Firefox, &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=866522"&gt;bookmarklets are subject to the site's CSP&lt;/a&gt;,
preventing bookmarklets from being run on many pages. In Chrome, bookmarklets
aren't subject to the CSP. But, by default, Chrome only shows bookmarks on the
New Tabs page; users must configure the bookmarks bar to be shown on web pages.&lt;/p&gt;
&lt;h2&gt;Browser Extensions (1999-)&lt;/h2&gt;
&lt;p&gt;The modern approach to browser extensibility, browser extensions, kicked off
in &lt;a href="https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa753620(v=vs.85)"&gt;1999 when Internet Explorer added support in Internet Explorer 4&lt;/a&gt;.
The new extensions supported &amp;quot;Explorer Bars&amp;quot; (i.e., the toolbar) and added
entries to context menus.&lt;/p&gt;
&lt;p&gt;Browser extensions, also sometimes called add-ons, support modification of the
browser interface, modification of a webpage's user interface, and running
JavaScript in the context of a webpage. These days, browser extensions support a
wide variety of use cases ranging
from &lt;a href="https://chrome.google.com/webstore/detail/grammarly-for-chrome/kbfnbcaeplbcioakkpcpgfkobkghlhen"&gt;writing assistance&lt;/a&gt;
and &lt;a href="https://chrome.google.com/webstore/detail/honey/bmnlcjabgnpnenekpadlanbbkooimhnj"&gt;web coupons&lt;/a&gt;
to &lt;a href="https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm"&gt;adblocking&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Firefox launched with extension support in
2004, &lt;a href="https://en.wikipedia.org/wiki/Browser_extension#History"&gt;with the other mainstream browsers following suit in 2009-2010&lt;/a&gt;.
As early as 2005, the &amp;quot;Mozilla Update&amp;quot; page was bustling with activity:&lt;/p&gt;
&lt;p&gt;&lt;img src="/assets/images/browser-extensibility/mozilla-addons.webp" alt="addons.mozilla.org in March 2005 via the Internet Archive" loading="lazy" decoding="async" /&gt;&lt;/p&gt;
&lt;p&gt;The major browser vendors each maintain their own browser extension marketplace
where extensions undergo a security review and updates can be automatically
distributed to users.&lt;/p&gt;
&lt;p&gt;Unfortunately, because browser extensions have access to a user's browsing
activity, they're an attractive attack vector for malicious actors.&lt;/p&gt;
&lt;p&gt;To curb abuse, Chrome, in particular, has taken steps to restrict how browser
extensions are distributed. In 2014, Chrome introduced restrictions so
that &lt;a href="https://techcrunch.com/2014/05/27/chrome-for-windows-will-now-only-install-extensions-from-googles-web-store/"&gt;Windows users could only install extensions from the Chrome Web Store&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Mozilla XUL (1997-2017)&lt;/h2&gt;
&lt;p&gt;Mozilla and Firefox's user interface was implemented using a language called
XUL, the XML User Interface Language. The XUL defines a user interface that is
rendered on the fly.&lt;/p&gt;
&lt;p&gt;In Firefox, browser extensions could modify the XUL, thus allowing them to
change core parts of the user interface (e.g., adding toolbars).&lt;/p&gt;
&lt;p&gt;Firefox discontinued XUL for extensions in 2017 because it led to
vulnerabilities and instability when multiple extensions modified the UX.
Technical users can still make limited modifications to the Firefox interface
via its &lt;a href="https://www.userchrome.org/"&gt;userChrome.css settings file&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Alternative Browser Distributions (2004-)&lt;/h2&gt;
&lt;p&gt;The Netscape browser was open-sourced in 1998 and used to build the Mozilla
browser suite. &lt;a href="https://en.wikipedia.org/wiki/Firefox"&gt;Mozilla Firefox&lt;/a&gt;, the
first mainstream open-source browser, was subsequently released in 2004.&lt;/p&gt;
&lt;p&gt;In 2008, Google released the Chromium open-source browser project. Google uses
Chromium as the basis for its Chrome web browser. By being open-source,
developers can &amp;quot;fork&amp;quot; the browser, modify it, and distribute their own custom
versions.&lt;/p&gt;
&lt;p&gt;There have been a lot
of &lt;a href="https://en.wikipedia.org/wiki/List_of_web_browsers"&gt;alternative browsers&lt;/a&gt;,
many of which have not stood the test of time. Some notable examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GNU IceWeasel/IceCat (2005-): Firefox distribution without proprietary
plug-ins.&lt;/li&gt;
&lt;li&gt;Flock (2005-2011): Browser with integrated social media features. Originally
based on Firefox and later based on Chromium. Was purchased by Zynga, a social
games company.&lt;/li&gt;
&lt;li&gt;Tor Browser Bundle (2008-): Firefox distribution that makes use of the Tor
network for private browsing.&lt;/li&gt;
&lt;li&gt;Brave (2016-): Brendan Eich's (of JavaScript fame) Chromium browser with
content-creator revenue sharing via the Basic Attention Token cryptocurrency.&lt;/li&gt;
&lt;li&gt;Microsoft Edge (2019-): Microsoft's Chromium-based browser after their
in-house browser (also called Edge) failed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="/assets/images/browser-extensibility/flock-browser.webp" alt="The Flock browser had social media features built in" loading="lazy" decoding="async" /&gt;&lt;/p&gt;
&lt;h2&gt;Userscripts (2005-)&lt;/h2&gt;
&lt;p&gt;Userscripts are small scripts that modify a webpage. Like browser extensions,
they're typically written in JavaScript. However, userscripts are lighter weight
in that you don't have to package them and distribute them through the browser's
web store. Instead, to run and manage userscripts, you use a browser extension
aptly called a userscript manager.&lt;/p&gt;
&lt;p&gt;Userscripts provide many benefits over bookmarklets: they can run automatically
on a page, aren't subject to length restrictions, and can run outside of a
site's CSP.&lt;/p&gt;
&lt;p&gt;The first userscript
manager, &lt;a href="https://en.wikipedia.org/wiki/Greasemonkey"&gt;Greasemonkey&lt;/a&gt;, was
released for Firefox in 2005. There are now several variations to choose from,
including Tampermonkey (2010) and Violentmonkey (2013). These variations all use
the same Greasemonkey-style scripts.&lt;/p&gt;
&lt;p&gt;In 2019, Firefox
added &lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/userScripts"&gt;formal support for userscripts&lt;/a&gt;
in extensions, providing additional security guarantees. Chromium has a separate
sandbox feature to run code in isolation, but has no formal userscript support.
However, as I will touch on a bit later, the end is nigh for userscripts in
Chromium browsers.&lt;/p&gt;
&lt;h2&gt;Converging on the WebExtensions API (2017-)&lt;/h2&gt;
&lt;p&gt;Abandoning XUL-based extensions in 2017, Firefox switched to the defacto
standard WebExtensions API by and large mimicking Chromium's API.&lt;/p&gt;
&lt;p&gt;Firefox's shift means that the vast majority of browser extensions are now using
the APIs defined by Chrome's Manifest Version 2, first released way back in
2012 (although additional incremental API functionality has been introduced
since then).&lt;/p&gt;
&lt;p&gt;Due to its waning market share, Firefox chose to support the &lt;code&gt;chrome&lt;/code&gt; API
namespace in addition to the vendor-agnostic &lt;code&gt;browser&lt;/code&gt; API namespace. In many
cases, Chrome extensions can be ported over to Firefox with no code
modifications. Firefox also
provides
&lt;a href="https://github.com/mozilla/webextension-polyfill"&gt;a polyfill for developers to make their vendor-agnostic code compatible with Chrome&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Writing extensions for Safari traditionally
involved &lt;a href="https://developer.apple.com/documentation/safariservices/safari_app_extensions/building_a_safari_app_extension"&gt;writing them using Apple's XCode&lt;/a&gt;
development tools. Starting with Safari 14 in 2020, Safari
also &lt;a href="https://developer.apple.com/news/?id=kuswih5l"&gt;adopted the WebExtensions API&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Manifest V3 (2021-)&lt;/h2&gt;
&lt;p&gt;In January 2021, Chrome released its controversial Manifest Version 3 (V3) of
the WebExtensions APIs. &amp;quot;Manifest&amp;quot; refers to both the technical integration
points and the policies Chrome enforces when reviewing extensions for its Chrome
Web Store.&lt;/p&gt;
&lt;p&gt;In line with previous browser API changes, the stated goals of Manifest V3 are
security, speed, and reliability. The major changes introduced in Manifest V3
are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Making network request modification declarative (speed/security)&lt;/li&gt;
&lt;li&gt;Using service workers for background operations, vs. a persistent background
page (reliability)&lt;/li&gt;
&lt;li&gt;Banning remotely hosted JavaScript code, a common vector of security
vulnerabilities (security)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Manifest V3
is &lt;a href="https://en.wikipedia.org/wiki/Google_Chrome#Reception"&gt;controversial in the browser extension community&lt;/a&gt;
because it breaks the functionality of many popular extensions.&lt;/p&gt;
&lt;p&gt;For the general public, the most worrisome is that popular content and
adblockers (e.g., uBlock Origin and Privacy Badger) will no longer be supported.
These blockers work by maintaining a large ruleset to block network requests.
Such large and dynamic rulesets are not supported in Manifest V3. (To be fair,
in response to the outrage, the Chrome team has increased the number of rules
per extension.)&lt;/p&gt;
&lt;p&gt;In addition, userscript managers, by their very definition, work by allowing
users to install and run remote JavaScript code. As of
now,
&lt;a href="https://github.com/violentmonkey/violentmonkey/issues/505"&gt;even user-pasted code will not be supported by Chrome once support for Manifest V2 is removed (likely in 2022)&lt;/a&gt;
.&lt;/p&gt;
&lt;h2&gt;The Future: No/Low-Code Browser Extension Builders (2021-)&lt;/h2&gt;
&lt;p&gt;Ever since mainstream browsers added support for extensions, the developer
community has built an incredible ecosystem of extensions that gives consumers
control over their browsing experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Style managers (2005-): Customize style of a page.&lt;/li&gt;
&lt;li&gt;Userscript managers (2005-2022?): Run custom code automatically.&lt;/li&gt;
&lt;li&gt;Content/adblockers (&lt;a href="https://en.wikipedia.org/wiki/Adblock_Plus#Background"&gt;2002&lt;/a&gt;-):
Block network requests and elements.&lt;/li&gt;
&lt;li&gt;Context menu managers (?-): Add context menu items (e.g., opening a new URL
dynamically based on the selected text).&lt;/li&gt;
&lt;li&gt;Web clippers (?-): Clip information from a page; customize what's clipped and
where it's sent.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Despite the number of extensions
published (&lt;a href="https://kommandotech.com/statistics/chrome-usage-statistics/"&gt;the Chrome Web Store has 189K extensions&lt;/a&gt;),
there is still a lot of room to innovate. In particular, the pairing of advances
in no-code + API-fication with browser extensions has the potential to increase
consumer productivity and self-efficacy dramatically.&lt;/p&gt;
&lt;p&gt;To realize this vision, I founded &lt;a href="https://www.pixiebrix.com/"&gt;PixieBrix&lt;/a&gt;. At
PixieBrix, we are working to create an open-source browser extension for
consumers and companies to customize their browsing experience to fit the way
they work. PixieBrix is no/low-code -- you configure and combine &amp;quot;bricks&amp;quot; to add
missing functionality, integrate sites without APIs, and automate repetitive
tasks.&lt;/p&gt;
&lt;p&gt;Personally, I'm very excited by the surge of interest in this space. One notable
startup I've come across is &lt;a href="https://insightbrowser.com/"&gt;Insight Browser&lt;/a&gt;, an
iOS browser that allows users to build and share simple extensions. In the
enterprise world, &lt;a href="https://extension.dev/"&gt;Extension.dev&lt;/a&gt; is another low-code
builder for internal enterprise extensions (check out their
comprehensive &amp;quot;&lt;a href="https://public.extension.dev/what-internal-extensions-are-people-building"&gt;What internal extensions are people building?&lt;/a&gt;&amp;quot;
page).&lt;/p&gt;
&lt;p&gt;If you're interested in working or investing in this space,
I'd &lt;a href="mailto:todd@pixiebrix.com"&gt;love to hear from you&lt;/a&gt;. Want to follow us on our
journey?
&lt;a href="https://twitter.com/pixiebrix"&gt;Follow us on Twitter&lt;/a&gt;,
&lt;a href="https://github.com/pixiebrix/pixiebrix-extension/"&gt;star our GitHub repository&lt;/a&gt;,
or &lt;a href="https://www.pixiebrix.com/"&gt;subscribe to our newsletter&lt;/a&gt;.&lt;/p&gt;
</content><category term="Browser Extensions"></category><category term="web browsers"></category><category term="browser extensions"></category><category term="history"></category></entry><entry><title>7 bite-sized tips for reliable web automation and scraping selectors</title><link href="https://toddschiller.com/blog/reliable-web-scrapers.html" rel="alternate"></link><published>2020-12-03T00:00:00-05:00</published><updated>2020-12-03T00:00:00-05:00</updated><author><name>Todd Schiller</name></author><id>tag:toddschiller.com,2020-12-03:/blog/reliable-web-scrapers.html</id><summary type="html">&lt;p&gt;If you're like most developers, you've probably
encountered &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors"&gt;Cascading Style Sheet (CSS) selectors&lt;/a&gt;
for styling webpages.&lt;/p&gt;
&lt;p&gt;For example, the following CSS rule combines a paragraph element selector &lt;code&gt;p&lt;/code&gt;
with a class name selector &lt;code&gt;.lead&lt;/code&gt; to set the font size:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;lead&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can mix-and-match CSS selectors to …&lt;/p&gt;</summary><content type="html">&lt;p&gt;If you're like most developers, you've probably
encountered &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors"&gt;Cascading Style Sheet (CSS) selectors&lt;/a&gt;
for styling webpages.&lt;/p&gt;
&lt;p&gt;For example, the following CSS rule combines a paragraph element selector &lt;code&gt;p&lt;/code&gt;
with a class name selector &lt;code&gt;.lead&lt;/code&gt; to set the font size:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;lead&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="kt"&gt;px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can mix-and-match CSS selectors to describe any subset of elements on a page.
There are CSS selectors for HTML tag types, ids, classes, attributes, page
structure, and even UX interactions.&lt;/p&gt;
&lt;p&gt;Because of their expressiveness, CSS selectors are used everywhere in the web
ecosystem:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Web
Applications (&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector"&gt;Web API&lt;/a&gt;,
&lt;a href="https://api.jquery.com/category/selectors/"&gt;JQuery&lt;/a&gt;):
modifying the DOM, attaching event handlers, etc.&lt;/li&gt;
&lt;li&gt;Automated Testing: finding elements to interact with, checking assertions,
etc.&lt;/li&gt;
&lt;li&gt;Web Scraping: selecting data to extract, finding links to traverse, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Over at PixieBrix, selectors are an integral part
of &lt;a href="https://www.pixiebrix.com/"&gt;our web customization engine&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Unfortunately, if we don't control a web page, writing reliable selectors can be
an unpleasant experience. Some old-school WYSIWYG-built sites (I'm looking at
you, &lt;a href="https://en.wikipedia.org/wiki/Microsoft_FrontPage"&gt;Microsoft FrontPage&lt;/a&gt;)
and modern Single Page Applications (SPAs) can be downright hostile to work
with.&lt;/p&gt;
&lt;p&gt;To help folks out, I've compiled a set of tips I find useful when writing
selectors for enhancing, automating, and scraping 3rd-party sites.&lt;/p&gt;
&lt;h2&gt;Tip #0: Use JQuery extensions&lt;/h2&gt;
&lt;p&gt;When working in a browser context (normal or headless),
use &lt;a href="https://api.jquery.com/category/selectors/jquery-selector-extensions/"&gt;JQuery's selector extensions&lt;/a&gt;.
Selectors such as &lt;code&gt;:eq&lt;/code&gt; , &lt;code&gt;:header&lt;/code&gt; , and &lt;code&gt;:contains&lt;/code&gt; make many selections
significantly more straightforward.&lt;/p&gt;
&lt;p&gt;A downside is that JQuery extensions are slower because they can't leverage the
browser's native CSS evaluation. However, for automation and scraping, you
probably won't ever notice. If it does become a problem, there are
simple &lt;a href="https://learn.jquery.com/performance/optimize-selectors/"&gt;techniques to optimize JQuery selectors&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In addition to CSS+JQuery, it's also helpful to learn a bit
of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/XPath"&gt;XPath&lt;/a&gt;, an XML/HTML
traversal language that browsers support natively.&lt;/p&gt;
&lt;h2&gt;Tip #1: Avoid structure-based selectors&lt;/h2&gt;
&lt;p&gt;Browsers and some &lt;a href="https://github.com/fczbkk/css-selector-generator"&gt;libraries&lt;/a&gt;
are able to automatically generate unique CSS selectors for elements.&lt;/p&gt;
&lt;p&gt;&lt;img src="/assets/images/web-selectors/chrome-css-selector.webp" alt="Generating a unique CSS selector for an element in Chrome" loading="lazy" decoding="async" /&gt;&lt;/p&gt;
&lt;p&gt;Automatic generators are great because they can take advantage of the structure
of the Document Object Model (DOM). Automatic generators are also terrible
because they sometimes rely too much on the structure of the DOM.&lt;/p&gt;
&lt;p&gt;Recently, a tool gave me the following selector:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;5&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;2&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;nth-child&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Structural selectors like these are extremely sensitive to small changes on the
page, including non-visible changes. (For example: an element with
&lt;code&gt;display:none&lt;/code&gt; could be inserted before an element.)&lt;/p&gt;
&lt;p&gt;These selectors will silently return the wrong element, and we'll wind up
debugging a failed automated test, or ingesting bad extracted data.&lt;/p&gt;
&lt;p&gt;Therefore, whenever possible, I eschew overly-specific structural selectors in
favor of ids, class names, attributes, and textual content.&lt;/p&gt;
&lt;h2&gt;Tip #2: Avoid dynamically generated attributes&lt;/h2&gt;
&lt;p&gt;In modern Single Page Applications (SPAs), element ids and class names are
commonly dynamically generated.&lt;/p&gt;
&lt;p&gt;Dynamic class names are computed when the code is compiled/bundled. That's
because the class names in the HTML file must match the names in separate CSS
stylesheets. Dynamic class names will change whenever the site is updated. (
Unless they are computed deterministically, e.g., using a hash. Then they'll
change whenever the style is changed).&lt;/p&gt;
&lt;p&gt;For example, take the &amp;quot;Google Search&amp;quot; button on
the &lt;a href="https://www.google.com"&gt;Google homepage&lt;/a&gt;. It's built
with &lt;a href="https://developers.google.com/closure"&gt;Google's Closure Framework&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;input&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;gNO89b&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Google Search&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;btnK&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;submit&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;At first glance, the &lt;code&gt;name=&amp;quot;btnK&amp;quot;&lt;/code&gt; also looks random, but it's not. The &amp;quot;I'm
Feeling Lucky&amp;quot; button button on the page has the name &lt;code&gt;btnI&lt;/code&gt;, suggesting they're
human-picked. As a rule of thumb, &lt;code&gt;input&lt;/code&gt; name attributes can't be dynamic
because other parts of the application (either front-end, or backend) depend on
them.&lt;/p&gt;
&lt;p&gt;A common source of dynamic class names in applications is the use
of &lt;a href="https://css-tricks.com/css-modules-part-1-need/"&gt;CSS Modules&lt;/a&gt;. CSS Modules
automatically encapsulating styles to avoid accidental styling collisions.
Here's an example header:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;_styles__title_309571057&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;An example heading&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;While we can't match the exact class name, it will follow a consistent naming
pattern across updates of the site. Therefore, we can match on the pattern
using &lt;a href="https://www.w3schools.com/css/css_attribute_selectors.asp"&gt;CSS's starts-with attribute selector&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;h1&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;class&lt;/span&gt;&lt;span class="o"&gt;^=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;_styles__title_&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Dynamic ids, on the other hand, are managed by the SPA framework at runtime.
Therefore, they'll change for each run of the application. Take, for example,
the profile header from LinkedIn, which uses
the &lt;a href="https://emberjs.com/"&gt;Ember.js&lt;/a&gt; framework:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;ember1398&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;pv-top-card artdeco-card ember-view&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="cm"&gt;&amp;lt;!-- more elements --&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here, the ember1398 id isn't random — the framework sequentially generates a new
id for each element it renders. In practice, the id of an element will differ
between page loads because elements don't always load in the exact same order.&lt;/p&gt;
&lt;h2&gt;Tip #3: Search text with JQuery's &lt;code&gt;:contains&lt;/code&gt; selector&lt;/h2&gt;
&lt;p&gt;JQuery's &lt;a href="https://api.jquery.com/contains-selector/"&gt;contains selector&lt;/a&gt; selects
elements that contain the given text anywhere within them. The selector is not
available in plain CSS, but it is available as
an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/XPath/Functions/contains"&gt;XPath function&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For example, consider the following HTML:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Lorem ipsum dolor sit amet, consectetur ...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We could select this element with:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Lorem ipsum&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If there are other tags, we need to add additional selectors to clarify which
element we want. For example, the above selector would select the &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;,
and &lt;code&gt;b&lt;/code&gt; elements in this document:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;b&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Lorem ipsum&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;b&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; dolor sit amet, consectetur...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;p&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To select just the &lt;code&gt;div&lt;/code&gt;, we'd need to specify the tag in the selector:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Lorem ipsum&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In some cases, the text might contain HTML entities, e.g., a non-breaking space:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Lorem&lt;span class="ni"&gt;&amp;amp;nsbp;&lt;/span&gt;ipsum&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Trying to select &amp;quot;Lorem ipsum&amp;quot; won't match here. In these cases, we provide the
contains selector multiple times to match only elements that contain both words:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Lorem&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ipsum&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, a caveat/warning: &lt;em&gt;when writing text selectors, be aware of which text
is translated when the page is viewed in a different language.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Tip #4: Target selection with &lt;code&gt;:has&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Sometimes we need more precision in targeting a text search. In these cases we
can combine a &lt;code&gt;:contains&lt;/code&gt; selector within a CSS &lt;code&gt;:has&lt;/code&gt; selector. Suppose we
wanted to get the year a property was built
from &lt;a href="https://www.apartments.com/the-max-new-york-ny/24f34qb/"&gt;Apartments.com&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;specList&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Property Information&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
         &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bullet&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;•&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Built in 2018&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
         &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;bullet&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;•&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;1016 Units/44 Stories&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can find the div with the Property Information header, and then grab the list
item corresponding to the year the property was built:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;specList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;h3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Property Information&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Built&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;:has&lt;/code&gt; selector can also be used to find the containing element. The
following selector matches a list containing an item with &amp;quot;Built&amp;quot;, rather than
the individual list item:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;ul&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;has&lt;/span&gt;&lt;span class="o"&gt;(&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Built&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Tip #5: Use ARIA attributes for more readable selectors&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA"&gt;Accessible Rich Internet Applications (ARIA)&lt;/a&gt;
attributes support the accessibility of a site, e.g., for providing context to
screen readers. Using them for selectors also makes selectors more readable.&lt;/p&gt;
&lt;p&gt;For example, the &lt;code&gt;aria-label&lt;/code&gt; attribute labels elements that otherwise would
depend on visual cues:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;aria-label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;Close&amp;quot;&lt;/span&gt; &lt;span class="na"&gt;onclick&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;myDialog.close()&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;X&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;These can be selected against using standard
CSS &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors"&gt;attribute selectors&lt;/a&gt;,
e.g.:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;aria-label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Close&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One thing to watch out for with the &lt;code&gt;aria-label&lt;/code&gt; attribute is that it's always
internationalized/translated alongside other UX text.&lt;/p&gt;
&lt;p&gt;ARIA attributes also provide information about the role of elements on the
page (e.g.,
the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/button_role"&gt;role attribute&lt;/a&gt;)
and even the relationship between elements on the page (
e.g., &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_aria-labelledby_attribute"&gt;aria-labelledby&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Tip #6: Be on the lookout for automated testing attributes&lt;/h2&gt;
&lt;p&gt;Developers using automated testing frameworks, e.g.
Cypress, &lt;a href="https://docs.cypress.io/guides/references/best-practices.html"&gt;add data attributes to make their life easier when writing automated tests&lt;/a&gt;.
Sometimes these attributes will find their way to the production site:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;data-test-id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cm"&gt;&amp;lt;!-- some content --&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;data-cy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="cm"&gt;&amp;lt;!-- some content --&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Testing attributes can be selected just like any other attribute, and are often
unique (just like an id):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nt"&gt;data-cy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;content&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="/assets/images/web-selectors/together.webp" alt="We're all in this together" loading="lazy" decoding="async" /&gt;&lt;/p&gt;
&lt;h2&gt;Bonus Tip: handle flat key/value pairs with the adjacent sibling combinator&lt;/h2&gt;
&lt;p&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Adjacent_sibling_combinator"&gt;adjacent sibling combinator (+)&lt;/a&gt;
matches the element immediately following another element.&lt;/p&gt;
&lt;p&gt;Supposed we have the following HTML, structured as multiple key-value pairs per
row/list item:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;row&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;k&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Name:&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;v&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Laura Smith&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;k&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Year:&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;v&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;2013&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can look up the value for the year by finding the &amp;quot;Year:&amp;quot; label and then
selecting the next element, which will be the value:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nd"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Year:&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;Also see the
&lt;a href="https://news.ycombinator.com/item?id=25993258"&gt;Hacker News discussion&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</content><category term="Browser Extensions"></category><category term="web browsers"></category><category term="automation"></category><category term="scraping"></category></entry><entry><title>The PixieBrix manifesto: web customization for the masses</title><link href="https://toddschiller.com/blog/pixiebrix-manifesto.html" rel="alternate"></link><published>2020-09-02T00:00:00-04:00</published><updated>2020-09-02T00:00:00-04:00</updated><author><name>Todd Schiller</name></author><id>tag:toddschiller.com,2020-09-02:/blog/pixiebrix-manifesto.html</id><summary type="html">&lt;p&gt;My freshman year of college, I interned as an &amp;quot;equities analyst&amp;quot;. What this
meant in practice was that I was supposed to look up numbers in a terminal, and
then type them into an Excel spreadsheet. The software provider hadn't gotten
around to implementing export for the data yet.&lt;/p&gt;
&lt;p&gt;Not …&lt;/p&gt;</summary><content type="html">&lt;p&gt;My freshman year of college, I interned as an &amp;quot;equities analyst&amp;quot;. What this
meant in practice was that I was supposed to look up numbers in a terminal, and
then type them into an Excel spreadsheet. The software provider hadn't gotten
around to implementing export for the data yet.&lt;/p&gt;
&lt;p&gt;Not wanting to spend a whole summer entering data, I set out to put my computer
science classes to use. I coded up a flow where I took screenshots, passed them
through off-the-shelf OCR software, and then had a script add them to the
spreadsheet. The two-month project took one week, and I spent the rest of the
summer enjoying World Cup Soccer matches on the office TV.&lt;/p&gt;
&lt;p&gt;That's when I realized, given the right tools, even basic coding skills can
translate into massive productivity and quality of life gains.&lt;/p&gt;
&lt;p&gt;Now almost 15 years later, I see a mixed landscape. Improvements in development
tools and AI are creating new possibilities. However, non-devs are often worse
off. Perpetual licenses and open-source software have given way to subscriptions
and walled gardens. Software providers artificially limit the productivity of
their software by designing for the average user. Or worse, intentionally
restrict functionality.&lt;/p&gt;
&lt;p&gt;We started PixieBrix because we believe three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There is no such thing as an &amp;quot;average user&amp;quot;&lt;/li&gt;
&lt;li&gt;Everyone should be empowered to customize their software to give them
superpowers&lt;/li&gt;
&lt;li&gt;Computing should be delightful&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To this end, we've set out to create low/no-code tools for customizing the
behavior of software, web pages, and SaaS. But we won't be able to do it alone.
That's why we're also working hard to build a diverse and vibrant community of
makers.&lt;/p&gt;
&lt;p&gt;Our first PixieBrix product is a low/no-code platform for extending web
browsers. Web browsers are the new OS, and everyday people are constantly
switching between countless web pages and SaaS apps. Now, with a bit of
configuration, anyone using our platform can add functionality that previously
required custom browser extension development. This ranges from adding elements
to a page, to automating workflows à la robotic process automation (RPA).&lt;/p&gt;
&lt;p&gt;Currently, building new features in our system requires common coding skills
such as YAML, CSS selectors, JSON Schema, and regular expressions. Over time,
we'll be working hard to reduce the barriers to use, mix, match, and create.&lt;/p&gt;
&lt;p&gt;Fitting with our beliefs, I'm proud to say
our &lt;a href="https://github.com/pixiebrix/pixiebrix-extension"&gt;browser extension is open source (GPL v3)&lt;/a&gt;.
That means everyone can inspect what code they're running and modify it, as long
as they're willing to extend that benefit to others.&lt;/p&gt;
&lt;p&gt;I'd like to personally invite you to join us on our journey. To keep up to date,
subscribe to our newsletter. And, if you have an idea you'd like to build, send
me an email at &lt;a href="mailto:todd@pixiebrix.com"&gt;todd@pixiebrix.com&lt;/a&gt;. I'd love to hear
about it and help turn it into a reality.&lt;/p&gt;
</content><category term="Browser Extensions"></category><category term="web browsers"></category><category term="customization"></category><category term="browser extensions"></category></entry></feed>