home-fasthtml / docs /unpublished /tutorial_for_web_devs.html
AItool's picture
Upload 210 files
036b3a6 verified
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head>
<meta charset="utf-8">
<meta name="generator" content="quarto-1.6.40">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<meta name="description" content="Learn the foundations of FastHTML by creating your own blogging system from scratch.">
<title>BYO Blog – fasthtml</title>
<style>
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
ul.task-list{list-style: none;}
ul.task-list li input[type="checkbox"] {
width: 0.8em;
margin: 0 0.8em 0.2em -1em; /* quarto-specific, see https://github.com/quarto-dev/quarto-cli/issues/4556 */
vertical-align: middle;
}
/* CSS for syntax highlighting */
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
.sourceCode { overflow: visible; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { display: inline-block; text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
}
pre.numberSource { margin-left: 3em; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
</style>
<script src="../site_libs/quarto-nav/quarto-nav.js"></script>
<script src="../site_libs/quarto-nav/headroom.min.js"></script>
<script src="../site_libs/clipboard/clipboard.min.js"></script>
<script src="../site_libs/quarto-search/autocomplete.umd.js"></script>
<script src="../site_libs/quarto-search/fuse.min.js"></script>
<script src="../site_libs/quarto-search/quarto-search.js"></script>
<meta name="quarto:offset" content="../">
<link href="../favicon.ico" rel="icon">
<script src="../site_libs/quarto-html/quarto.js"></script>
<script src="../site_libs/quarto-html/popper.min.js"></script>
<script src="../site_libs/quarto-html/tippy.umd.min.js"></script>
<script src="../site_libs/quarto-html/anchor.min.js"></script>
<link href="../site_libs/quarto-html/tippy.css" rel="stylesheet">
<link href="../site_libs/quarto-html/quarto-syntax-highlighting-549806ee2085284f45b00abea8c6df48.css" rel="stylesheet" id="quarto-text-highlighting-styles">
<script src="../site_libs/bootstrap/bootstrap.min.js"></script>
<link href="../site_libs/bootstrap/bootstrap-icons.css" rel="stylesheet">
<link href="../site_libs/bootstrap/bootstrap-e463c5e664eae906a5c2eb38a07ecc3d.min.css" rel="stylesheet" append-hash="true" id="quarto-bootstrap" data-mode="light">
<script id="quarto-search-options" type="application/json">{
"location": "navbar",
"copy-button": false,
"collapse-after": 3,
"panel-placement": "end",
"type": "overlay",
"limit": 50,
"keyboard-shortcut": [
"f",
"/",
"s"
],
"show-item-context": false,
"language": {
"search-no-results-text": "No results",
"search-matching-documents-text": "matching documents",
"search-copy-link-title": "Copy link to search",
"search-hide-matches-text": "Hide additional matches",
"search-more-match-text": "more match in this document",
"search-more-matches-text": "more matches in this document",
"search-clear-button-title": "Clear",
"search-text-placeholder": "",
"search-detached-cancel-button-title": "Cancel",
"search-submit-button-title": "Submit",
"search-label": "Search"
}
}</script>
<link rel="stylesheet" href="../styles.css">
<meta property="og:title" content="BYO Blog – fasthtml">
<meta property="og:description" content="Learn the foundations of FastHTML by creating your own blogging system from scratch.">
<meta property="og:image" content="https://www.fastht.ml/docs/unpublished/quickstart-web-dev/quickstart-fasthtml.png">
<meta property="og:site_name" content="fasthtml">
<meta name="twitter:title" content="BYO Blog – fasthtml">
<meta name="twitter:description" content="Learn the foundations of FastHTML by creating your own blogging system from scratch.">
<meta name="twitter:image" content="https://www.fastht.ml/docs/og-image.png">
<meta name="twitter:creator" content="@jeremyphoward">
<meta name="twitter:site" content="@answerdotai">
<meta name="twitter:card" content="summary_large_image">
<link rel="canonical" href="https://www.fastht.ml/docs/unpublished/tutorial_for_web_devs.html">
</head>
<body class="nav-sidebar floating nav-fixed">
<div id="quarto-search-results"></div>
<header id="quarto-header" class="headroom fixed-top">
<nav class="navbar navbar-expand-lg " data-bs-theme="dark">
<div class="navbar-container container-fluid">
<div class="navbar-brand-container mx-auto">
<a href="../index.html" class="navbar-brand navbar-brand-logo">
<img src="../logo.svg" alt="" class="navbar-logo">
</a>
</div>
<div id="quarto-search" class="" title="Search"></div>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" role="menu" aria-expanded="false" aria-label="Toggle navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav navbar-nav-scroll me-auto">
<li class="nav-item">
<a class="nav-link" href="https://fastht.ml">
<span class="menu-text">Home</span></a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://fastht.ml/about">
<span class="menu-text">Learn</span></a>
</li>
</ul>
<ul class="navbar-nav navbar-nav-scroll ms-auto">
<li class="nav-item compact">
<a class="nav-link" href="https://github.com/answerdotai/fasthtml"> <i class="bi bi-github" role="img">
</i>
<span class="menu-text"></span></a>
</li>
<li class="nav-item compact">
<a class="nav-link" href="https://x.com/answerdotai"> <i class="bi bi-twitter" role="img" aria-label="Fast.ai Twitter">
</i>
<span class="menu-text"></span></a>
</li>
</ul>
</div> <!-- /navcollapse -->
<div class="quarto-navbar-tools">
</div>
</div> <!-- /container-fluid -->
</nav>
<nav class="quarto-secondary-nav">
<div class="container-fluid d-flex">
<button type="button" class="quarto-btn-toggle btn" data-bs-toggle="collapse" role="button" data-bs-target=".quarto-sidebar-collapse-item" aria-controls="quarto-sidebar" aria-expanded="false" aria-label="Toggle sidebar navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }">
<i class="bi bi-layout-text-sidebar-reverse"></i>
</button>
<nav class="quarto-page-breadcrumbs" aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item">BYO Blog</li></ol></nav>
<a class="flex-grow-1" role="navigation" data-bs-toggle="collapse" data-bs-target=".quarto-sidebar-collapse-item" aria-controls="quarto-sidebar" aria-expanded="false" aria-label="Toggle sidebar navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }">
</a>
</div>
</nav>
</header>
<!-- content -->
<div id="quarto-content" class="quarto-container page-columns page-rows-contents page-layout-article page-navbar">
<!-- sidebar -->
<nav id="quarto-sidebar" class="sidebar collapse collapse-horizontal quarto-sidebar-collapse-item sidebar-navigation floating overflow-auto">
<div class="sidebar-menu-container">
<ul class="list-unstyled mt-1">
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../index.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Get Started</span></a>
</div>
</li>
<li class="sidebar-item sidebar-item-section">
<div class="sidebar-item-container">
<a href="../tutorials/index.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Tutorials</span></a>
<a class="sidebar-item-toggle text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-1" role="navigation" aria-expanded="true" aria-label="Toggle section">
<i class="bi bi-chevron-right ms-2"></i>
</a>
</div>
<ul id="quarto-sidebar-section-1" class="collapse list-unstyled sidebar-section depth1 show">
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../tutorials/by_example.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">FastHTML By Example</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../tutorials/quickstart_for_web_devs.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Web Devs Quickstart</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../tutorials/e2e.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">JS App Walkthrough</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../tutorials/best_practice.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">FastHTML Best Practices</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../tutorials/jupyter_and_fasthtml.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Using Jupyter to write FastHTML</span></a>
</div>
</li>
</ul>
</li>
<li class="sidebar-item sidebar-item-section">
<div class="sidebar-item-container">
<a class="sidebar-item-text sidebar-link text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-2" role="navigation" aria-expanded="true">
<span class="menu-text">Explanations</span></a>
<a class="sidebar-item-toggle text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-2" role="navigation" aria-expanded="true" aria-label="Toggle section">
<i class="bi bi-chevron-right ms-2"></i>
</a>
</div>
<ul id="quarto-sidebar-section-2" class="collapse list-unstyled sidebar-section depth1 show">
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../explains/background_tasks.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Background Tasks</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../explains/explaining_xt_components.html" class="sidebar-item-text sidebar-link">
<span class="menu-text"><strong>FT</strong> Components</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../explains/faq.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">FAQ</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../explains/minidataapi.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">MiniDataAPI Spec</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../explains/oauth.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">OAuth</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../explains/routes.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Routes</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../explains/stripe.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Stripe</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../explains/websockets.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">WebSockets</span></a>
</div>
</li>
</ul>
</li>
<li class="sidebar-item sidebar-item-section">
<div class="sidebar-item-container">
<a class="sidebar-item-text sidebar-link text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-3" role="navigation" aria-expanded="true">
<span class="menu-text">Reference</span></a>
<a class="sidebar-item-toggle text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-3" role="navigation" aria-expanded="true" aria-label="Toggle section">
<i class="bi bi-chevron-right ms-2"></i>
</a>
</div>
<ul id="quarto-sidebar-section-3" class="collapse list-unstyled sidebar-section depth1 show">
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../ref/concise_guide.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Concise reference</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../ref/best_practice.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">FastHTML Best Practices</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../ref/defining_xt_component.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Custom Components</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../ref/handlers.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Handling handlers</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../ref/live_reload.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Live Reloading</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../ref/response_types.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Response Types</span></a>
</div>
</li>
</ul>
</li>
<li class="sidebar-item sidebar-item-section">
<div class="sidebar-item-container">
<a class="sidebar-item-text sidebar-link text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-4" role="navigation" aria-expanded="true">
<span class="menu-text">Source</span></a>
<a class="sidebar-item-toggle text-start" data-bs-toggle="collapse" data-bs-target="#quarto-sidebar-section-4" role="navigation" aria-expanded="true" aria-label="Toggle section">
<i class="bi bi-chevron-right ms-2"></i>
</a>
</div>
<ul id="quarto-sidebar-section-4" class="collapse list-unstyled sidebar-section depth1 show">
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/core.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Core</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/components.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Components</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/xtend.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Component extensions</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/js.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Javascript examples</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/pico.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Pico.css components</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/svg.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">SVG</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/jupyter.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Jupyter compatibility</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/oauth.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">OAuth</span></a>
</div>
</li>
<li class="sidebar-item">
<div class="sidebar-item-container">
<a href="../api/cli.html" class="sidebar-item-text sidebar-link">
<span class="menu-text">Command Line Tools</span></a>
</div>
</li>
</ul>
</li>
</ul>
</div>
</nav>
<div id="quarto-sidebar-glass" class="quarto-sidebar-collapse-item" data-bs-toggle="collapse" data-bs-target=".quarto-sidebar-collapse-item"></div>
<!-- margin-sidebar -->
<div id="quarto-margin-sidebar" class="sidebar margin-sidebar">
<nav id="TOC" role="doc-toc" class="toc-active">
<h2 id="toc-title">On this page</h2>
<ul>
<li><a href="#how-to-best-use-this-tutorial" id="toc-how-to-best-use-this-tutorial" class="nav-link active" data-scroll-target="#how-to-best-use-this-tutorial">How to best use this tutorial</a></li>
<li><a href="#installing-fasthtml" id="toc-installing-fasthtml" class="nav-link" data-scroll-target="#installing-fasthtml">Installing FastHTML</a></li>
<li><a href="#a-minimal-fasthtml-app" id="toc-a-minimal-fasthtml-app" class="nav-link" data-scroll-target="#a-minimal-fasthtml-app">A minimal FastHTML app</a></li>
<li><a href="#looking-more-closely-at-our-app" id="toc-looking-more-closely-at-our-app" class="nav-link" data-scroll-target="#looking-more-closely-at-our-app">Looking more closely at our app</a></li>
<li><a href="#adding-dynamic-content-to-our-minimal-app" id="toc-adding-dynamic-content-to-our-minimal-app" class="nav-link" data-scroll-target="#adding-dynamic-content-to-our-minimal-app">Adding dynamic content to our minimal app</a></li>
<li><a href="#storing-the-articles" id="toc-storing-the-articles" class="nav-link" data-scroll-target="#storing-the-articles">Storing the articles</a></li>
<li><a href="#creating-the-blog-home-page" id="toc-creating-the-blog-home-page" class="nav-link" data-scroll-target="#creating-the-blog-home-page">Creating the blog home page</a></li>
</ul>
<div class="toc-actions"><ul><li><a href="https://github.com/AnswerDotAI/fasthtml/issues/new" class="toc-action"><i class="bi bi-github"></i>Report an issue</a></li></ul></div><div class="quarto-alternate-formats"><h2>Other Formats</h2><ul><li><a href="tutorial_for_web_devs.html.md"><i class="bi bi-file-code"></i>CommonMark</a></li></ul></div></nav>
</div>
<!-- main -->
<main class="content" id="quarto-document-content">
<header id="title-block-header" class="quarto-title-block default">
<div class="quarto-title">
<h1 class="title">BYO Blog</h1>
</div>
<div>
<div class="description">
Learn the foundations of FastHTML by creating your own blogging system from scratch.
</div>
</div>
<div class="quarto-title-meta">
</div>
</header>
<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->
<div class="callout callout-style-default callout-caution callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Caution
</div>
</div>
<div class="callout-body-container callout-body">
<p>This document is a work in progress.</p>
</div>
</div>
<p>In this tutorial we’re going to write a blog by example. Blogs are a good way to learn a web framework as they start simple yet can get surprisingly sophistated. The <a href="https://en.wikipedia.org/wiki/Blog">wikipedia definition of a blog</a> is “an informational website consisting of discrete, often informal diary-style text entries (posts) informal diary-style text entries (posts)”, which means we need to provide these basic features:</p>
<ul>
<li>A list of articles</li>
<li>A means to create/edit/delete the articles</li>
<li>An attractive but accessible layout</li>
</ul>
<p>We’ll also add in these features, so the blog can become a working site:</p>
<ul>
<li>RSS feed</li>
<li>Pages independent of the list of articles (about and contact come to mind)</li>
<li>Import and Export of articles</li>
<li>Tagging and categorization of data</li>
<li>Deployment</li>
<li>Ability to scale for large volumes of readers</li>
</ul>
<section id="how-to-best-use-this-tutorial" class="level2">
<h2 class="anchored" data-anchor-id="how-to-best-use-this-tutorial">How to best use this tutorial</h2>
<p>We could copy/paste every code example in sequence and have a finished blog at the end. However, it’s debatable how much we will learn through the copy/paste method. We’re not saying its impossible to learn through copy/paste, we’re just saying it’s not that of an efficient way to learn. It’s analogous to learning how to play a musical instrument or sport or video game by watching other people do it - you can learn some but its not the same as doing.</p>
<p>A better approach is to type out every line of code in this tutorial. This forces us to run the code through our brains, giving us actual practice in how to write FastHTML and Pythoncode and forcing us to debug our own mistakes. In some cases we’ll repeat similar tasks - a key component in achieving mastery in anything. Coming back to the instrument/sport/video game analogy, it’s exactly like actually practicing an instrument, sport, or video game. Through practice and repetition we eventually achieve mastery.</p>
</section>
<section id="installing-fasthtml" class="level2">
<h2 class="anchored" data-anchor-id="installing-fasthtml">Installing FastHTML</h2>
<p>FastHTML is <em>just Python</em>. Installation is often done with pip:</p>
<pre class="shellscript"><code>pip install python-fasthtml</code></pre>
</section>
<section id="a-minimal-fasthtml-app" class="level2">
<h2 class="anchored" data-anchor-id="a-minimal-fasthtml-app">A minimal FastHTML app</h2>
<p>First, create the directory for our project using Python’s <a href="https://docs.python.org/3/library/pathlib.html">pathlib</a> module:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pathlib</span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>pathlib.Path(<span class="st">'blog-system'</span>).mkdir()</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
<p>Now that we have our directory, let’s create a minimal FastHTML site in it.</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre><strong>blog-system/minimal.py</strong></pre>
</div>
<div class="sourceCode" id="cb3" data-filename="blog-system/minimal.py"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> fasthtml.common <span class="im">import</span> <span class="op">*</span> </span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>app, rt <span class="op">=</span> fast_app() </span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="at">@rt</span>(<span class="st">"/"</span>) </span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> get():</span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Titled(<span class="st">"FastHTML"</span>, P(<span class="st">"Let's do this!"</span>)) </span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a>serve()</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
</div>
<p>Run that with <code>python minimal.py</code> and you should get something like this:</p>
<pre class="shellscript"><code>python minimal.py
Link: http://localhost:5001
INFO: Will watch for changes in these directories: ['/Users/pydanny/projects/blog-system']
INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
INFO: Started reloader process [46572] using WatchFiles
INFO: Started server process [46576]
INFO: Waiting for application startup.
INFO: Application startup complete.</code></pre>
<p>Confirm FastHTML is running by opening your web browser to <a href="http://127.0.0.1:5001">127.0.0.1:5001</a>. You should see something like the image below:</p>
<p><img src="quickstart-web-dev/quickstart-fasthtml.png" class="img-fluid"></p>
<div class="callout callout-style-default callout-note callout-titled" title="What about the `import *`?">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
What about the <code>import *</code>?
</div>
</div>
<div class="callout-body-container callout-body">
<p>For those worried about the use of <code>import *</code> rather than a PEP8-style declared namespace, understand that <code>__all__</code> is defined in FastHTML’s common module. That means that only the symbols (functions, classes, and other things) the framework wants us to have will be brought into our own code via <code>import *</code>. Read <a href="https://docs.python.org/3/tutorial/modules.html#importing-from-a-package">importing from a package</a>) for more information.</p>
<p>Nevertheless, if we want to use a defined namespace we can do so. Here’s an example:</p>
<div class="sourceCode" id="cb5"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> fasthtml <span class="im">import</span> common <span class="im">as</span> fh</span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a>app, rt <span class="op">=</span> fh.fast_app() </span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="at">@rt</span>(<span class="st">"/"</span>) </span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> get():</span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> fh.Titled(<span class="st">"FastHTML"</span>, fh.P(<span class="st">"Let's do this!"</span>)) </span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a>fh.serve()</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
</div>
</div>
</section>
<section id="looking-more-closely-at-our-app" class="level2">
<h2 class="anchored" data-anchor-id="looking-more-closely-at-our-app">Looking more closely at our app</h2>
<p>Let’s look more closely at our application. Every line is packed with powerful features of FastHTML:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre><strong>blog-system/minimal.py</strong></pre>
</div>
<div class="sourceCode" id="annotated-cell-5" data-filename="blog-system/minimal.py"><pre class="sourceCode python code-annotation-code code-with-copy code-annotated"><code class="sourceCode python"><a class="code-annotation-anchor" data-target-cell="annotated-cell-5" data-target-annotation="1" onclick="event.preventDefault();">1</a><span id="annotated-cell-5-1" class="code-annotation-target"><a href="#annotated-cell-5-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> fasthtml.common <span class="im">import</span> <span class="op">*</span></span>
<span id="annotated-cell-5-2"><a href="#annotated-cell-5-2" aria-hidden="true" tabindex="-1"></a></span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-5" data-target-annotation="2" onclick="event.preventDefault();">2</a><span id="annotated-cell-5-3" class="code-annotation-target"><a href="#annotated-cell-5-3" aria-hidden="true" tabindex="-1"></a>app, rt <span class="op">=</span> fast_app()</span>
<span id="annotated-cell-5-4"><a href="#annotated-cell-5-4" aria-hidden="true" tabindex="-1"></a></span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-5" data-target-annotation="3" onclick="event.preventDefault();">3</a><span id="annotated-cell-5-5" class="code-annotation-target"><a href="#annotated-cell-5-5" aria-hidden="true" tabindex="-1"></a><span class="at">@rt</span>(<span class="st">"/"</span>)</span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-5" data-target-annotation="4" onclick="event.preventDefault();">4</a><span id="annotated-cell-5-6" class="code-annotation-target"><a href="#annotated-cell-5-6" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> get():</span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-5" data-target-annotation="5" onclick="event.preventDefault();">5</a><span id="annotated-cell-5-7" class="code-annotation-target"><a href="#annotated-cell-5-7" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Titled(<span class="st">"FastHTML"</span>, P(<span class="st">"Let's do this!"</span>))</span>
<span id="annotated-cell-5-8"><a href="#annotated-cell-5-8" aria-hidden="true" tabindex="-1"></a></span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-5" data-target-annotation="6" onclick="event.preventDefault();">6</a><span id="annotated-cell-5-9" class="code-annotation-target"><a href="#annotated-cell-5-9" aria-hidden="true" tabindex="-1"></a>serve()</span><div class="code-annotation-gutter-bg"></div><div class="code-annotation-gutter"></div></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
</div>
<dl class="code-annotation-container-grid">
<dt data-target-cell="annotated-cell-5" data-target-annotation="1">1</dt>
<dd>
<span data-code-cell="annotated-cell-5" data-code-lines="1" data-code-annotation="1">The top level namespace of Fast HTML (fasthtml.common) contains everything we need from FastHTML to build applications. A carefully-curated set of FastHTML functions and other Python objects is brought into our global namespace for convenience.</span>
</dd>
<dt data-target-cell="annotated-cell-5" data-target-annotation="2">2</dt>
<dd>
<span data-code-cell="annotated-cell-5" data-code-lines="3" data-code-annotation="2">We instantiate a FastHTML app with the <code>fast_app()</code> utility function. This provides a number of really useful defaults that we’ll modify or take advantage of later in the tutorial.</span>
</dd>
<dt data-target-cell="annotated-cell-5" data-target-annotation="3">3</dt>
<dd>
<span data-code-cell="annotated-cell-5" data-code-lines="5" data-code-annotation="3">We use the <code>rt()</code> decorator to tell FastHTML what to return when a user visits <code>/</code> in their browser.</span>
</dd>
<dt data-target-cell="annotated-cell-5" data-target-annotation="4">4</dt>
<dd>
<span data-code-cell="annotated-cell-5" data-code-lines="6" data-code-annotation="4">We connect this route to HTTP GET requests by defining a view function called <code>get()</code>.</span>
</dd>
<dt data-target-cell="annotated-cell-5" data-target-annotation="5">5</dt>
<dd>
<span data-code-cell="annotated-cell-5" data-code-lines="7" data-code-annotation="5">A tree of Python function calls that return all the HTML required to write a properly formed web page. You’ll soon see the power of this approach.</span>
</dd>
<dt data-target-cell="annotated-cell-5" data-target-annotation="6">6</dt>
<dd>
<span data-code-cell="annotated-cell-5" data-code-lines="9" data-code-annotation="6">The <a href="https://www.fastht.ml/docs/api/core.html#serve"><code>serve()</code></a> utility configures and runs FastHTML using a library called <code>uvicorn</code>. Any changes to this module will be reloaded into the browser.</span>
</dd>
</dl>
</section>
<section id="adding-dynamic-content-to-our-minimal-app" class="level2">
<h2 class="anchored" data-anchor-id="adding-dynamic-content-to-our-minimal-app">Adding dynamic content to our minimal app</h2>
<p>Our page is great, but we’ll make it better. Let’s add a randomized list of letters to the page. Every time the page reloads, a new list of varying length will be generated.</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre><strong>blog-system/random_letters.py</strong></pre>
</div>
<div class="sourceCode" id="annotated-cell-6" data-filename="blog-system/random_letters.py"><pre class="sourceCode numberSource python code-annotation-code number-lines code-with-copy code-annotated"><code class="sourceCode python"><span id="annotated-cell-6-1"><a href="#annotated-cell-6-1"></a><span class="im">from</span> fasthtml.common <span class="im">import</span> <span class="op">*</span></span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-6" data-target-annotation="1" onclick="event.preventDefault();">1</a><span id="annotated-cell-6-2" class="code-annotation-target"><a href="#annotated-cell-6-2"></a><span class="im">import</span> string, random</span>
<span id="annotated-cell-6-3"><a href="#annotated-cell-6-3"></a></span>
<span id="annotated-cell-6-4"><a href="#annotated-cell-6-4"></a>app, rt <span class="op">=</span> fast_app()</span>
<span id="annotated-cell-6-5"><a href="#annotated-cell-6-5"></a></span>
<span id="annotated-cell-6-6"><a href="#annotated-cell-6-6"></a><span class="at">@rt</span>(<span class="st">"/"</span>)</span>
<span id="annotated-cell-6-7"><a href="#annotated-cell-6-7"></a><span class="kw">def</span> get():</span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-6" data-target-annotation="2" onclick="event.preventDefault();">2</a><span id="annotated-cell-6-8" class="code-annotation-target"><a href="#annotated-cell-6-8"></a> letters <span class="op">=</span> random.choices(string.ascii_uppercase, k<span class="op">=</span>random.randint(<span class="dv">5</span>, <span class="dv">20</span>))</span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-6" data-target-annotation="3" onclick="event.preventDefault();">3</a><span id="annotated-cell-6-9" class="code-annotation-target"><a href="#annotated-cell-6-9"></a> items <span class="op">=</span> [Li(c) <span class="cf">for</span> c <span class="kw">in</span> letters]</span>
<span id="annotated-cell-6-10"><a href="#annotated-cell-6-10"></a> <span class="cf">return</span> Titled(<span class="st">"Random lists of letters"</span>,</span>
<a class="code-annotation-anchor" data-target-cell="annotated-cell-6" data-target-annotation="4" onclick="event.preventDefault();">4</a><span id="annotated-cell-6-11" class="code-annotation-target"><a href="#annotated-cell-6-11"></a> Ul(<span class="op">*</span>items)</span>
<span id="annotated-cell-6-12"><a href="#annotated-cell-6-12"></a> ) </span>
<span id="annotated-cell-6-13"><a href="#annotated-cell-6-13"></a></span>
<span id="annotated-cell-6-14"><a href="#annotated-cell-6-14"></a>serve()</span><div class="code-annotation-gutter-bg"></div><div class="code-annotation-gutter"></div></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
</div>
<dl class="code-annotation-container-grid">
<dt data-target-cell="annotated-cell-6" data-target-annotation="1">1</dt>
<dd>
<span data-code-cell="annotated-cell-6" data-code-lines="2" data-code-annotation="1">The <code>string</code> and <code>random</code> libraries are part of Python’s standard library</span>
</dd>
<dt data-target-cell="annotated-cell-6" data-target-annotation="2">2</dt>
<dd>
<span data-code-cell="annotated-cell-6" data-code-lines="8" data-code-annotation="2">We use these libraries to generate a random length list of random letters called <code>letters</code></span>
</dd>
<dt data-target-cell="annotated-cell-6" data-target-annotation="3">3</dt>
<dd>
<span data-code-cell="annotated-cell-6" data-code-lines="9" data-code-annotation="3">Using <code>letters</code> as the base we use list comprehension to generate a list of <code>Li</code> ft display components, each with their own letter and save that to the variable <code>items</code></span>
</dd>
<dt data-target-cell="annotated-cell-6" data-target-annotation="4">4</dt>
<dd>
<span data-code-cell="annotated-cell-6" data-code-lines="11" data-code-annotation="4">Inside a call to the <code>Ul()</code> ft component we use Python’s <code>*args</code> special syntax on the <code>items</code> variable. Therefore <code>*list</code> is treated not as one argument but rather a set of them.</span>
</dd>
</dl>
<p>When this is run, it will generate something like this with a different random list of letters for each page load:</p>
<p><img src="web-dev-tut/random-list-letters.png" class="img-fluid"></p>
</section>
<section id="storing-the-articles" class="level2">
<h2 class="anchored" data-anchor-id="storing-the-articles">Storing the articles</h2>
<p>The most basic component of a blog is a series of articles sorted by date authored. Rather than a database we’re going to use our computer’s harddrive to store a set of markdown files in a directory within our blog called <code>posts</code>. First, let’s create the directory and some test files we can use to search for:</p>
<div id="cell-10" class="cell">
<div class="sourceCode cell-code" id="cb6"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> fastcore.utils <span class="im">import</span> <span class="op">*</span></span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
</div>
<div id="cell-11" class="cell">
<div class="sourceCode cell-code" id="cb7"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Create some dummy posts</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>posts <span class="op">=</span> Path(<span class="st">"posts"</span>)</span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a>posts.mkdir(exist_ok<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">10</span>): (posts<span class="op">/</span><span class="ss">f"article_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">.md"</span>).write_text(<span class="ss">f"This is article </span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">"</span>)</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
</div>
<p>Searching for these files can be done with pathlib.</p>
<div id="cell-13" class="cell">
<div class="sourceCode cell-code" id="cb8"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pathlib</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a>posts.ls()</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
<div class="cell-output cell-output-display">
<pre><code>(#10) [Path('posts/article_5.md'),Path('posts/article_1.md'),Path('posts/article_0.md'),Path('posts/article_4.md'),Path('posts/article_3.md'),Path('posts/article_7.md'),Path('posts/article_6.md'),Path('posts/article_2.md'),Path('posts/article_9.md'),Path('posts/article_8.md')]</code></pre>
</div>
</div>
<div class="callout callout-style-default callout-tip callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Tip
</div>
</div>
<div class="callout-body-container callout-body">
<p>Python’s <a href="https://docs.python.org/3/library/pathlib.html">pathlib</a> library is quite useful and makes file search and manipulation much easier. There’s many uses for it and is compatible across operating systems.</p>
</div>
</div>
</section>
<section id="creating-the-blog-home-page" class="level2">
<h2 class="anchored" data-anchor-id="creating-the-blog-home-page">Creating the blog home page</h2>
<p>We now have enough tools that we can create the home page. Let’s create a new Python file and write out our simple view to list the articles in our blog.</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre><strong>blog-system/main.py</strong></pre>
</div>
<div class="sourceCode" id="annotated-cell-10" data-filename="blog-system/main.py"><pre class="sourceCode numberSource python code-annotation-code number-lines code-with-copy"><code class="sourceCode python"><span id="annotated-cell-10-1"><a href="#annotated-cell-10-1"></a><span class="im">from</span> fasthtml.common <span class="im">import</span> <span class="op">*</span></span>
<span id="annotated-cell-10-2"><a href="#annotated-cell-10-2"></a><span class="im">import</span> pathlib</span>
<span id="annotated-cell-10-3"><a href="#annotated-cell-10-3"></a></span>
<span id="annotated-cell-10-4"><a href="#annotated-cell-10-4"></a>app, rt <span class="op">=</span> fast_app()</span>
<span id="annotated-cell-10-5"><a href="#annotated-cell-10-5"></a></span>
<span id="annotated-cell-10-6"><a href="#annotated-cell-10-6"></a><span class="at">@rt</span>(<span class="st">"/"</span>)</span>
<span id="annotated-cell-10-7"><a href="#annotated-cell-10-7"></a><span class="kw">def</span> get():</span>
<span id="annotated-cell-10-8"><a href="#annotated-cell-10-8"></a> fnames <span class="op">=</span> pathlib.Path(<span class="st">"posts"</span>).rglob(<span class="st">"*.md"</span>)</span>
<span id="annotated-cell-10-9"><a href="#annotated-cell-10-9"></a> items <span class="op">=</span> [Li(A(fname, href<span class="op">=</span>fname)) <span class="cf">for</span> fname <span class="kw">in</span> fnames] </span>
<span id="annotated-cell-10-10"><a href="#annotated-cell-10-10"></a> <span class="cf">return</span> Titled(<span class="st">"My Blog"</span>,</span>
<span id="annotated-cell-10-11"><a href="#annotated-cell-10-11"></a> Ul(<span class="op">*</span>items)</span>
<span id="annotated-cell-10-12"><a href="#annotated-cell-10-12"></a> ) </span>
<span id="annotated-cell-10-13"><a href="#annotated-cell-10-13"></a></span>
<span id="annotated-cell-10-14"><a href="#annotated-cell-10-14"></a>serve()</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
</div>
<div id="cell-17" class="cell">
<div class="sourceCode cell-code" id="cb10"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb10-1"><a href="#cb10-1" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> p <span class="kw">in</span> posts.ls(): p.unlink()</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div>
</div>
</section>
</main> <!-- /main -->
<script id="quarto-html-after-body" type="application/javascript">
window.document.addEventListener("DOMContentLoaded", function (event) {
const toggleBodyColorMode = (bsSheetEl) => {
const mode = bsSheetEl.getAttribute("data-mode");
const bodyEl = window.document.querySelector("body");
if (mode === "dark") {
bodyEl.classList.add("quarto-dark");
bodyEl.classList.remove("quarto-light");
} else {
bodyEl.classList.add("quarto-light");
bodyEl.classList.remove("quarto-dark");
}
}
const toggleBodyColorPrimary = () => {
const bsSheetEl = window.document.querySelector("link#quarto-bootstrap");
if (bsSheetEl) {
toggleBodyColorMode(bsSheetEl);
}
}
toggleBodyColorPrimary();
const icon = "";
const anchorJS = new window.AnchorJS();
anchorJS.options = {
placement: 'right',
icon: icon
};
anchorJS.add('.anchored');
const isCodeAnnotation = (el) => {
for (const clz of el.classList) {
if (clz.startsWith('code-annotation-')) {
return true;
}
}
return false;
}
const onCopySuccess = function(e) {
// button target
const button = e.trigger;
// don't keep focus
button.blur();
// flash "checked"
button.classList.add('code-copy-button-checked');
var currentTitle = button.getAttribute("title");
button.setAttribute("title", "Copied!");
let tooltip;
if (window.bootstrap) {
button.setAttribute("data-bs-toggle", "tooltip");
button.setAttribute("data-bs-placement", "left");
button.setAttribute("data-bs-title", "Copied!");
tooltip = new bootstrap.Tooltip(button,
{ trigger: "manual",
customClass: "code-copy-button-tooltip",
offset: [0, -8]});
tooltip.show();
}
setTimeout(function() {
if (tooltip) {
tooltip.hide();
button.removeAttribute("data-bs-title");
button.removeAttribute("data-bs-toggle");
button.removeAttribute("data-bs-placement");
}
button.setAttribute("title", currentTitle);
button.classList.remove('code-copy-button-checked');
}, 1000);
// clear code selection
e.clearSelection();
}
const getTextToCopy = function(trigger) {
const codeEl = trigger.previousElementSibling.cloneNode(true);
for (const childEl of codeEl.children) {
if (isCodeAnnotation(childEl)) {
childEl.remove();
}
}
return codeEl.innerText;
}
const clipboard = new window.ClipboardJS('.code-copy-button:not([data-in-quarto-modal])', {
text: getTextToCopy
});
clipboard.on('success', onCopySuccess);
if (window.document.getElementById('quarto-embedded-source-code-modal')) {
const clipboardModal = new window.ClipboardJS('.code-copy-button[data-in-quarto-modal]', {
text: getTextToCopy,
container: window.document.getElementById('quarto-embedded-source-code-modal')
});
clipboardModal.on('success', onCopySuccess);
}
var localhostRegex = new RegExp(/^(?:http|https):\/\/localhost\:?[0-9]*\//);
var mailtoRegex = new RegExp(/^mailto:/);
var filterRegex = new RegExp("https:\/\/www\.fastht\.ml\/docs\/");
var isInternal = (href) => {
return filterRegex.test(href) || localhostRegex.test(href) || mailtoRegex.test(href);
}
// Inspect non-navigation links and adorn them if external
var links = window.document.querySelectorAll('a[href]:not(.nav-link):not(.navbar-brand):not(.toc-action):not(.sidebar-link):not(.sidebar-item-toggle):not(.pagination-link):not(.no-external):not([aria-hidden]):not(.dropdown-item):not(.quarto-navigation-tool):not(.about-link)');
for (var i=0; i<links.length; i++) {
const link = links[i];
if (!isInternal(link.href)) {
// undo the damage that might have been done by quarto-nav.js in the case of
// links that we want to consider external
if (link.dataset.originalHref !== undefined) {
link.href = link.dataset.originalHref;
}
}
}
function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) {
const config = {
allowHTML: true,
maxWidth: 500,
delay: 100,
arrow: false,
appendTo: function(el) {
return el.parentElement;
},
interactive: true,
interactiveBorder: 10,
theme: 'quarto',
placement: 'bottom-start',
};
if (contentFn) {
config.content = contentFn;
}
if (onTriggerFn) {
config.onTrigger = onTriggerFn;
}
if (onUntriggerFn) {
config.onUntrigger = onUntriggerFn;
}
window.tippy(el, config);
}
const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]');
for (var i=0; i<noterefs.length; i++) {
const ref = noterefs[i];
tippyHover(ref, function() {
// use id or data attribute instead here
let href = ref.getAttribute('data-footnote-href') || ref.getAttribute('href');
try { href = new URL(href).hash; } catch {}
const id = href.replace(/^#\/?/, "");
const note = window.document.getElementById(id);
if (note) {
return note.innerHTML;
} else {
return "";
}
});
}
const xrefs = window.document.querySelectorAll('a.quarto-xref');
const processXRef = (id, note) => {
// Strip column container classes
const stripColumnClz = (el) => {
el.classList.remove("page-full", "page-columns");
if (el.children) {
for (const child of el.children) {
stripColumnClz(child);
}
}
}
stripColumnClz(note)
if (id === null || id.startsWith('sec-')) {
// Special case sections, only their first couple elements
const container = document.createElement("div");
if (note.children && note.children.length > 2) {
container.appendChild(note.children[0].cloneNode(true));
for (let i = 1; i < note.children.length; i++) {
const child = note.children[i];
if (child.tagName === "P" && child.innerText === "") {
continue;
} else {
container.appendChild(child.cloneNode(true));
break;
}
}
if (window.Quarto?.typesetMath) {
window.Quarto.typesetMath(container);
}
return container.innerHTML
} else {
if (window.Quarto?.typesetMath) {
window.Quarto.typesetMath(note);
}
return note.innerHTML;
}
} else {
// Remove any anchor links if they are present
const anchorLink = note.querySelector('a.anchorjs-link');
if (anchorLink) {
anchorLink.remove();
}
if (window.Quarto?.typesetMath) {
window.Quarto.typesetMath(note);
}
if (note.classList.contains("callout")) {
return note.outerHTML;
} else {
return note.innerHTML;
}
}
}
for (var i=0; i<xrefs.length; i++) {
const xref = xrefs[i];
tippyHover(xref, undefined, function(instance) {
instance.disable();
let url = xref.getAttribute('href');
let hash = undefined;
if (url.startsWith('#')) {
hash = url;
} else {
try { hash = new URL(url).hash; } catch {}
}
if (hash) {
const id = hash.replace(/^#\/?/, "");
const note = window.document.getElementById(id);
if (note !== null) {
try {
const html = processXRef(id, note.cloneNode(true));
instance.setContent(html);
} finally {
instance.enable();
instance.show();
}
} else {
// See if we can fetch this
fetch(url.split('#')[0])
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(html, "text/html");
const note = htmlDoc.getElementById(id);
if (note !== null) {
const html = processXRef(id, note);
instance.setContent(html);
}
}).finally(() => {
instance.enable();
instance.show();
});
}
} else {
// See if we can fetch a full url (with no hash to target)
// This is a special case and we should probably do some content thinning / targeting
fetch(url)
.then(res => res.text())
.then(html => {
const parser = new DOMParser();
const htmlDoc = parser.parseFromString(html, "text/html");
const note = htmlDoc.querySelector('main.content');
if (note !== null) {
// This should only happen for chapter cross references
// (since there is no id in the URL)
// remove the first header
if (note.children.length > 0 && note.children[0].tagName === "HEADER") {
note.children[0].remove();
}
const html = processXRef(null, note);
instance.setContent(html);
}
}).finally(() => {
instance.enable();
instance.show();
});
}
}, function(instance) {
});
}
let selectedAnnoteEl;
const selectorForAnnotation = ( cell, annotation) => {
let cellAttr = 'data-code-cell="' + cell + '"';
let lineAttr = 'data-code-annotation="' + annotation + '"';
const selector = 'span[' + cellAttr + '][' + lineAttr + ']';
return selector;
}
const selectCodeLines = (annoteEl) => {
const doc = window.document;
const targetCell = annoteEl.getAttribute("data-target-cell");
const targetAnnotation = annoteEl.getAttribute("data-target-annotation");
const annoteSpan = window.document.querySelector(selectorForAnnotation(targetCell, targetAnnotation));
const lines = annoteSpan.getAttribute("data-code-lines").split(",");
const lineIds = lines.map((line) => {
return targetCell + "-" + line;
})
let top = null;
let height = null;
let parent = null;
if (lineIds.length > 0) {
//compute the position of the single el (top and bottom and make a div)
const el = window.document.getElementById(lineIds[0]);
top = el.offsetTop;
height = el.offsetHeight;
parent = el.parentElement.parentElement;
if (lineIds.length > 1) {
const lastEl = window.document.getElementById(lineIds[lineIds.length - 1]);
const bottom = lastEl.offsetTop + lastEl.offsetHeight;
height = bottom - top;
}
if (top !== null && height !== null && parent !== null) {
// cook up a div (if necessary) and position it
let div = window.document.getElementById("code-annotation-line-highlight");
if (div === null) {
div = window.document.createElement("div");
div.setAttribute("id", "code-annotation-line-highlight");
div.style.position = 'absolute';
parent.appendChild(div);
}
div.style.top = top - 2 + "px";
div.style.height = height + 4 + "px";
div.style.left = 0;
let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter");
if (gutterDiv === null) {
gutterDiv = window.document.createElement("div");
gutterDiv.setAttribute("id", "code-annotation-line-highlight-gutter");
gutterDiv.style.position = 'absolute';
const codeCell = window.document.getElementById(targetCell);
const gutter = codeCell.querySelector('.code-annotation-gutter');
gutter.appendChild(gutterDiv);
}
gutterDiv.style.top = top - 2 + "px";
gutterDiv.style.height = height + 4 + "px";
}
selectedAnnoteEl = annoteEl;
}
};
const unselectCodeLines = () => {
const elementsIds = ["code-annotation-line-highlight", "code-annotation-line-highlight-gutter"];
elementsIds.forEach((elId) => {
const div = window.document.getElementById(elId);
if (div) {
div.remove();
}
});
selectedAnnoteEl = undefined;
};
// Handle positioning of the toggle
window.addEventListener(
"resize",
throttle(() => {
elRect = undefined;
if (selectedAnnoteEl) {
selectCodeLines(selectedAnnoteEl);
}
}, 10)
);
function throttle(fn, ms) {
let throttle = false;
let timer;
return (...args) => {
if(!throttle) { // first call gets through
fn.apply(this, args);
throttle = true;
} else { // all the others get throttled
if(timer) clearTimeout(timer); // cancel #2
timer = setTimeout(() => {
fn.apply(this, args);
timer = throttle = false;
}, ms);
}
};
}
// Attach click handler to the DT
const annoteDls = window.document.querySelectorAll('dt[data-target-cell]');
for (const annoteDlNode of annoteDls) {
annoteDlNode.addEventListener('click', (event) => {
const clickedEl = event.target;
if (clickedEl !== selectedAnnoteEl) {
unselectCodeLines();
const activeEl = window.document.querySelector('dt[data-target-cell].code-annotation-active');
if (activeEl) {
activeEl.classList.remove('code-annotation-active');
}
selectCodeLines(clickedEl);
clickedEl.classList.add('code-annotation-active');
} else {
// Unselect the line
unselectCodeLines();
clickedEl.classList.remove('code-annotation-active');
}
});
}
const findCites = (el) => {
const parentEl = el.parentElement;
if (parentEl) {
const cites = parentEl.dataset.cites;
if (cites) {
return {
el,
cites: cites.split(' ')
};
} else {
return findCites(el.parentElement)
}
} else {
return undefined;
}
};
var bibliorefs = window.document.querySelectorAll('a[role="doc-biblioref"]');
for (var i=0; i<bibliorefs.length; i++) {
const ref = bibliorefs[i];
const citeInfo = findCites(ref);
if (citeInfo) {
tippyHover(citeInfo.el, function() {
var popup = window.document.createElement('div');
citeInfo.cites.forEach(function(cite) {
var citeDiv = window.document.createElement('div');
citeDiv.classList.add('hanging-indent');
citeDiv.classList.add('csl-entry');
var biblioDiv = window.document.getElementById('ref-' + cite);
if (biblioDiv) {
citeDiv.innerHTML = biblioDiv.innerHTML;
}
popup.appendChild(citeDiv);
});
return popup.innerHTML;
});
}
}
});
</script>
</div> <!-- /content -->
<footer class="footer"><div class="nav-footer"><div class="nav-footer-center"><div class="toc-actions d-sm-block d-md-none"><ul><li><a href="https://github.com/AnswerDotAI/fasthtml/issues/new" class="toc-action"><i class="bi bi-github"></i>Report an issue</a></li></ul></div></div></div></footer></body></html>