component, and we’ll have a nice blank page.
\\n\\n\\n\\nFor styling, let’s add Tailwind and some very basic markup to the homepage to contain our site.
\\n\\n\\n\\nnpx astro add tailwind
\\n\\n\\n\\nThe astro add
command will install Tailwind and attempt to set up all the boilerplate code for you (handy!). The CLI will ask you if you want it to add the various components, I recommend letting it, but if anything fails, you can copy the code needed from each of the steps in the process. As the last step for getting to work with Tailwind, the CLI will tell you to import the styles into a shared layout. Follow those instructions, and we can get to work.
Let’s add some very basic markup to our new homepage.
\\n\\n\\n\\n---\\n// ./src/pages/index.astro\\nimport Layout from \'../layouts/Layout.astro\';\\n---\\n\\n<Layout>\\n <div class=\\"max-w-3xl mx-auto my-10\\">\\n <h1 class=\\"text-3xl text-center\\">My latest bookmarks</h1>\\n <p class=\\"text-xl text-center mb-5\\">This is only 10 of A LARGE NUMBER THAT WE\'LL CHANGE LATER</p>\\n </div>\\n</Layout>
\\n\\n\\n\\nYour site should now look like this.
\\n\\n\\n\\nNot exactly winning any awards yet! That’s alright. Let’s get our bookmarks loaded in.
\\n\\n\\nSince not everyone runs their own application for bookmarking interesting items, you can borrow my data. Here’s a small subset of my bookmarks, or you can go get 110 items from this link on GitHub. Add this data as a file in your project. I like to group data in a data
directory, so my file lives in /src/data/bookmarks.json
.
[\\n {\\n \\"pageTitle\\": \\"Our Favorite Sandwich Bread | King Arthur Baking\\",\\n \\"url\\": \\"<https://www.kingarthurbaking.com/recipes/our-favorite-sandwich-bread-recipe>\\",\\n \\"description\\": \\"Classic American sandwich loaf, perfect for French toast and sandwiches.\\",\\n \\"id\\": \\"007y8pmEOvhwldfT3wx1MW\\"\\n },\\n {\\n \\"pageTitle\\": \\"Chris Coyier\'s discussion of Automatic Social Share Images | CSS-Tricks \\",\\n \\"url\\": \\"<https://css-tricks.com/automatic-social-share-images/>\\",\\n \\"description\\": \\"It\'s a pretty low-effort thing to get a big fancy link preview on social media. Toss a handful of specific <meta> tags on a URL and you get a big image-title-description thing \\",\\n \\"id\\": \\"04CXDvGQo19m0oXERL6bhF\\"\\n },\\n {\\n \\"pageTitle\\": \\"Automatic Social Share Images | ryanfiller.com\\",\\n \\"url\\": \\"<https://www.ryanfiller.com/blog/automatic-social-share-images/>\\",\\n \\"description\\": \\"Setting up automatic social share images with Puppeteer and Netlify Functions. \\",\\n \\"id\\": \\"04CXDvGQo19m0oXERLoC10\\"\\n },\\n {\\n \\"pageTitle\\": \\"Emma Wedekind: Foundations of Design Systems / React Boston 2019 - YouTube\\",\\n \\"url\\": \\"<https://m.youtube.com/watch?v=pXb2jA43A6k>\\",\\n \\"description\\": \\"Emma Wedekind: Foundations of Design Systems / React Boston 2019 Presented by: Emma Wedekind – LogMeIn Design systems are in the world around us, from street...\\",\\n \\"id\\": \\"0d56d03e-aba4-4ebd-9db8-644bcc185e33\\"\\n },\\n {\\n \\"pageTitle\\": \\"Editorial Design Patterns With CSS Grid And Named Columns — Smashing Magazine\\",\\n \\"url\\": \\"<https://www.smashingmagazine.com/2019/10/editorial-design-patterns-css-grid-subgrid-naming/>\\",\\n \\"description\\": \\"By naming lines when setting up our CSS Grid layouts, we can tap into some interesting and useful features of Grid — features that become even more powerful when we introduce subgrids.\\",\\n \\"id\\": \\"13ac1043-1b7d-4a5b-a3d8-b6f5ec34cf1c\\"\\n },\\n {\\n \\"pageTitle\\": \\"Netlify pro tip: Using Split Testing to power private beta releases - DEV Community 👩💻👨💻\\",\\n \\"url\\": \\"<https://dev.to/philhawksworth/netlify-pro-tip-using-split-testing-to-power-private-beta-releases-a7l>\\",\\n \\"description\\": \\"Giving users ways to opt in and out of your private betas. Video and tutorial.\\",\\n \\"id\\": \\"1fbabbf9-2952-47f2-9005-25af90b0229e\\"\\n },\\n {\\n \\"pageTitle\\": \\"Netlify Public Folder, Part I: What? Recreating the Dropbox Public Folder With Netlify | Jim Nielsen’s Weblog\\",\\n \\"url\\": \\"<https://blog.jim-nielsen.com/2019/netlify-public-folder-part-i-what/>\\",\\n\\n \\"id\\": \\"2607e651-7b64-4695-8af9-3b9b88d402d5\\"\\n },\\n {\\n \\"pageTitle\\": \\"Why Is CSS So Weird? - YouTube\\",\\n \\"url\\": \\"<https://m.youtube.com/watch?v=aHUtMbJw8iA&feature=youtu.be>\\",\\n \\"description\\": \\"Love it or hate it, CSS is weird! It doesn\'t work like most programming languages, and it doesn\'t work like a design tool either. But CSS is also solving a v...\\",\\n \\"id\\": \\"2e29aa3b-45b8-4ce4-85b7-fd8bc50daccd\\"\\n },\\n {\\n \\"pageTitle\\": \\"Internet world despairs as non-profit .org sold for $$$$ to private equity firm, price caps axed • The Register\\",\\n \\"url\\": \\"<https://www.theregister.co.uk/2019/11/20/org_registry_sale_shambles/>\\",\\n\\n \\"id\\": \\"33406b33-c453-44d3-8b18-2d2ae83ee73f\\"\\n },\\n {\\n \\"pageTitle\\": \\"Netlify Identity for paid subscriptions - Access Control / Identity - Netlify Community\\",\\n \\"url\\": \\"<https://community.netlify.com/t/netlify-identity-for-paid-subscriptions/1947/2>\\",\\n \\"description\\": \\"I want to limit certain functionality on my website to paying users. Now I’m using a payment provider (Mollie) similar to Stripe. My idea was to use the webhook fired by this service to call a Netlify function and give…\\",\\n \\"id\\": \\"34d6341c-18eb-4744-88e1-cfbf6c1cfa6c\\"\\n },\\n {\\n \\"pageTitle\\": \\"SmashingConf Freiburg 2019: Videos And Photos — Smashing Magazine\\",\\n \\"url\\": \\"<https://www.smashingmagazine.com/2019/10/smashingconf-freiburg-2019/>\\",\\n \\"description\\": \\"We had a lovely time at SmashingConf Freiburg. This post wraps up the event and also shares the video of all of the Freiburg presentations.\\",\\n \\"id\\": \\"354cbb34-b24a-47f1-8973-8553ed1d809d\\"\\n },\\n {\\n \\"pageTitle\\": \\"Adding Google Calendar to your JAMStack\\",\\n \\"url\\": \\"<https://www.raymondcamden.com/2019/11/18/adding-google-calendar-to-your-jamstack>\\",\\n \\"description\\": \\"A look at using Google APIs to add events to your static site.\\",\\n \\"id\\": \\"361b20c4-75ce-46b3-b6d9-38139e03f2ca\\"\\n },\\n {\\n \\"pageTitle\\": \\"How to Contribute to an Open Source Project | CSS-Tricks\\",\\n \\"url\\": \\"<https://css-tricks.com/how-to-contribute-to-an-open-source-project/>\\",\\n \\"description\\": \\"The following is going to get slightly opinionated and aims to guide someone on their journey into open source. As a prerequisite, you should have basic\\",\\n \\"id\\": \\"37300606-af08-4d9a-b5e3-12f64ebbb505\\"\\n },\\n {\\n \\"pageTitle\\": \\"Functions | Netlify\\",\\n \\"url\\": \\"<https://www.netlify.com/docs/functions/>\\",\\n \\"description\\": \\"Netlify builds, deploys, and hosts your front end. Learn how to get started, see examples, and view documentation for the modern web platform.\\",\\n \\"id\\": \\"3bf9e31b-5288-4b3b-89f2-97034603dbf6\\"\\n },\\n {\\n \\"pageTitle\\": \\"Serverless Can Help You To Focus - By Simona Cotin\\",\\n \\"url\\": \\"<https://hackernoon.com/serverless-can-do-that-7nw32mk>\\",\\n\\n \\"id\\": \\"43b1ee63-c2f8-4e14-8700-1e21c2e0a8b1\\"\\n },\\n {\\n \\"pageTitle\\": \\"Nuxt, Next, Nest?! My Head Hurts. - DEV Community 👩💻👨💻\\",\\n \\"url\\": \\"<https://dev.to/laurieontech/nuxt-next-nest-my-head-hurts-5h98>\\",\\n \\"description\\": \\"I clearly know what all of these things are. Their names are not at all similar. But let\'s review, just to make sure we know...\\",\\n \\"id\\": \\"456b7d6d-7efa-408a-9eca-0325d996b69c\\"\\n },\\n {\\n \\"pageTitle\\": \\"Consuming a headless CMS GraphQL API with Eleventy - Webstoemp\\",\\n \\"url\\": \\"<https://www.webstoemp.com/blog/headless-cms-graphql-api-eleventy/>\\",\\n \\"description\\": \\"With Eleventy, consuming data coming from a GraphQL API to generate static pages is as easy as using Markdown files.\\",\\n \\"id\\": \\"4606b168-21a6-49df-8536-a2a00750d659\\"\\n },\\n]
\\n\\n\\nNow that the data is in the project, we need for Astro to incorporate the data into its build process. To do this, we can use Astro’s new(ish) Content Layer API. The Content Layer API adds a content configuration file to your src
directory that allows you to run and collect any number of content pieces from data in your project or external APIs. Create the file /src/content.config.ts
(the name of this file matters, as this is what Astro is looking for in your project).
import { defineCollection, z } from \\"astro:content\\";\\nimport { file } from \'astro/loaders\';\\n\\nconst bookmarks = defineCollection({\\n schema: z.object({\\n pageTitle: z.string(),\\n url: z.string(),\\n description: z.string().optional()\\n }),\\n loader: file(\\"src/data/bookmarks.json\\"),\\n});\\n\\nexport const collections = { bookmarks };
\\n\\n\\n\\nIn this file, we import a few helpers from Astro. We can use defineCollection
to create the collection, z
as Zod, to help define our types, and file
is a specific content loader meant to read data files.
The defineCollection
method takes an object as its argument with a required loader and optional schema. The schema will help make our content type-safe and make sure our data is always what we expect it to be. In this case, we’ll define the three data properties each of our bookmarks has. It’s important to define all your data in your schema, otherwise it won’t be available to your templates.
We provide the loader
property with a content loader. In this case, we’ll use the file
loader that Astro provides and give it the path to our JSON.
Finally, we need to export the collections
variable as an object containing all the collections that we’ve defined (just bookmarks
in our project). You’ll want to restart the local server by re-running npm run dev
in your terminal to pick up the new data.
Now that we have data, we can use it in our homepage to show the most recent bookmarks that have been added. To get the data, we need to access the content collection with the getCollection
method from astro:content
. Add the following code to the frontmatter for ./src/pages/index.astro
.
---\\nimport Layout from \'../layouts/Layout.astro\';\\nimport { getCollection } from \'astro:content\';\\n\\nconst bookmarks = await getCollection(\'bookmarks\');\\n---
\\n\\n\\n\\nThis code imports the getCollection
method and uses it to create a new variable that contains the data in our bookmarks
collection. The bookmarks
variable is an array of data, as defined by the collection, which we can use to loop through in our template.
---\\nimport Layout from \'../layouts/Layout.astro\';\\nimport { getCollection } from \'astro:content\';\\n\\nconst bookmarks = await getCollection(\'bookmarks\');\\n---\\n\\n<Layout>\\n <div class=\\"max-w-3xl mx-auto my-10\\">\\n <h1 class=\\"text-3xl text-center\\">My latest bookmarks</h1>\\n <p class=\\"text-xl text-center mb-5\\">\\n This is only 10 of {bookmarks.length}\\n </p>\\n\\n <h2 class=\\"text-2xl mb-3\\">Latest bookmarks</h2>\\n <ul class=\\"grid gap-4\\">\\n {\\n bookmarks.slice(0, 10).map((item) => (\\n <li>\\n <a\\n href={item.data?.url}\\n class=\\"block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700\\">\\n <h3 class=\\"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white\\">\\n {item.data?.pageTitle}\\n </h3>\\n <p class=\\"font-normal text-gray-700 dark:text-gray-400\\">\\n {item.data?.description}\\n </p>\\n </a>\\n </li>\\n ))\\n }\\n </ul>\\n </div>\\n</Layout>
\\n\\n\\n\\nThis should pull the most recent 10 items from the array and display them on the homepage with some Tailwind styles. The main thing to note here is that the data structure has changed a little. The actual data for each item in our array actually resides in the data
property of the item. This allows Astro to put additional data on the object without colliding with any details we provide in our database. Your project should now look something like this.
Now that we have data and display, let’s get to work on our search functionality.
\\n\\n\\nTo start, we’ll want to scaffold out a new Astro component. In our example, we’re going to use vanilla JavaScript, but if you’re familiar with React or other frameworks that Astro supports, you can opt for client Islands to build out your search. The Astro actions will work the same.
\\n\\n\\nWe need to make a new component to house a bit of JavaScript and the HTML for the search field and results. Create the component in a ./src/components/Search.astro
file.
<form id=\\"searchForm\\" class=\\"flex mb-6 items-center max-w-sm mx-auto\\">\\n <label for=\\"simple-search\\" class=\\"sr-only\\">Search</label>\\n <div class=\\"relative w-full\\">\\n <input\\n type=\\"text\\"\\n id=\\"search\\"\\n class=\\"bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500\\"\\n placeholder=\\"Search Bookmarks\\"\\n required\\n />\\n </div>\\n <button\\n type=\\"submit\\"\\n class=\\"p-2.5 ms-2 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800\\">\\n <svg\\n class=\\"w-4 h-4\\"\\n aria-hidden=\\"true\\"\\n xmlns=\\"<http://www.w3.org/2000/svg>\\"\\n fill=\\"none\\"\\n viewBox=\\"0 0 20 20\\">\\n <path\\n stroke=\\"currentColor\\"\\n stroke-linecap=\\"round\\"\\n stroke-linejoin=\\"round\\"\\n stroke-width=\\"2\\"\\n d=\\"m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z\\"></path>\\n </svg>\\n <span class=\\"sr-only\\">Search</span>\\n </button>\\n</form>\\n\\n<div class=\\"grid gap-4 mb-10 hidden\\" id=\\"results\\">\\n <h2 class=\\"text-xl font-bold mb-2\\">Search Results</h2>\\n</div>\\n\\n<script>\\n const form = document.getElementById(\\"searchForm\\");\\n const search = document.getElementById(\\"search\\");\\n const results = document.getElementById(\\"results\\");\\n\\n form?.addEventListener(\\"submit\\", async (e) => {\\n e.preventDefault();\\n console.log(\\"SEARCH WILL HAPPEN\\");\\n });\\n</script>
\\n\\n\\n\\nThe basic HTML is setting up a search form, input, and results area with IDs that we’ll use in JavaScript. The basic JavaScript finds those elements, and for the form, adds an event listener that fires when the form is submitted. The event listener is where a lot of our magic is going to happen, but for now, a console log will do to make sure everything is set up properly.
\\n\\n\\nIn order for Actions to work, we need our project to allow for Astro to work in server or hybrid mode. These modes allow for all or some pages to be rendered in serverless functions instead of pre-generated as HTML during the build. In this project, this will be used for the Action and nothing else, so we’ll opt for hybrid mode.
\\n\\n\\n\\nTo be able to run Astro in this way, we need to add a server integration. Astro has integrations for most of the major cloud providers, as well as a basic Node implementation. I typically host on Netlify, so we’ll install their integration. Much like with Tailwind, we’ll use the CLI to add the package and it will build out the boilerplate we need.
\\n\\n\\n\\nnpx astro add netlify
\\n\\n\\n\\nOnce this is added, Astro is running in Hybrid mode. Most of our site is pre-generated with HTML, but when the Action gets used, it will run as a serverless function.
\\n\\n\\nNext, we need an Astro Action to handle our search functionality. To create the action, we need to create a new file at ./src/actions/index.js
. All our Actions live in this file. You can write the code for each one in separate files and import them into this file, but in this example, we only have one Action, and that feels like premature optimization.
In this file, we’ll set up our search Action. Much like setting up our content collections, we’ll use a method called defineAction
and give it a schema and in this case a handler. The schema will validate the data it’s getting from our JavaScript is typed correctly, and the handler will define what happens when the Action runs.
import { defineAction } from \\"astro:actions\\";\\nimport { z } from \\"astro:schema\\";\\nimport { getCollection } from \\"astro:content\\";\\n\\nexport const server = {\\n search: defineAction({\\n schema: z.object({\\n query: z.string(),\\n }),\\n handler: async (query) => {\\n const bookmarks = await getCollection(\\"bookmarks\\");\\n const results = await bookmarks.filter((bookmark) => {\\n return bookmark.data.pageTitle.includes(query);\\n });\\n return results;\\n },\\n }),\\n};
\\n\\n\\n\\nFor our Action, we’ll name it search
and expect a schema of an object with a single property named query
which is a string. The handler function will get all of our bookmarks from the content collection and use a native JavaScript .filter()
method to check if the query is included in any bookmark titles. This basic functionality is ready to test with our front-end.
When the user submits the form, we need to send the query to our new Action. Instead of figuring out where to send our fetch request, Astro gives us access to all of our server Actions with the actions
object in astro:actions
. This means that any Action we create is accessible from our client-side JavaScript.
In our Search component, we can now import our Action directly into the JavaScript and then use the search action when the user submits the form.
\\n\\n\\n\\n<script>\\nimport { actions } from \\"astro:actions\\";\\n\\nconst form = document.getElementById(\\"searchForm\\");\\nconst search = document.getElementById(\\"search\\");\\nconst results = document.getElementById(\\"results\\");\\n\\nform?.addEventListener(\\"submit\\", async (e) => {\\n e.preventDefault();\\n results.innerHTML = \\"\\";\\n\\n const query = search.value;\\n const { data, error } = await actions.search(query);\\n if (error) {\\n results.innerHTML = `<p>${error.message}</p>`;\\n return;\\n }\\n // create a div for each search result\\n data.forEach(( item ) => {\\n const div = document.createElement(\\"div\\");\\n div.innerHTML = `\\n <a href=\\"${item.data?.url}\\" class=\\"block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700\\">\\n <h3 class=\\"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white\\">\\n ${item.data?.pageTitle}\\n </h3>\\n <p class=\\"font-normal text-gray-700 dark:text-gray-400\\">\\n ${item.data?.description}\\n </p>\\n </a>`;\\n // append the div to the results container\\n results.appendChild(div);\\n });\\n // show the results container\\n results.classList.remove(\\"hidden\\");\\n});\\n</script>
\\n\\n\\n\\nWhen results are returned, we can now get search results!
\\n\\n\\n\\nThough, they’re highly problematic. This is just a simple JavaScript filter, after all. You can search for “Favorite” and get my favorite bread recipe, but if you search for “favorite” (no caps), you’ll get an error… Not ideal.
\\n\\n\\n\\nThat’s why we should use a package like Fuse.js.
\\n\\n\\nFuse.js is a JavaScript package that has utilities to make “fuzzy” search much easier for developers. Fuse will accept a string and based on a number of criteria (and a number of sets of data) provide responses that closely match even when the match isn’t perfect. Depending on the settings, Fuse can match “Favorite”, “favorite”, and even misspellings like “favrite” all to the right results.
\\n\\n\\n\\nIs Fuse as powerful as something like Algolia or ElasticSearch? No. Is it free and pretty darned good? Absolutely! To get Fuse moving, we need to install it into our project.
\\n\\n\\n\\nnpm install fuse.js
\\n\\n\\n\\nFrom there, we can use it in our Action by importing it in the file and creating a new instance of Fuse based on our bookmarks collection.
\\n\\n\\n\\nimport { defineAction } from \\"astro:actions\\";\\nimport { z } from \\"astro:schema\\";\\nimport { getCollection } from \\"astro:content\\";\\nimport Fuse from \\"fuse.js\\";\\n\\nexport const server = {\\n search: defineAction({\\n schema: z.object({\\n query: z.string(),\\n }),\\n handler: async (query) => {\\n const bookmarks = await getCollection(\\"bookmarks\\");\\n const fuse = new Fuse(bookmarks, {\\n threshold: 0.3,\\n keys: [\\n { name: \\"data.pageTitle\\", weight: 1.0 },\\n { name: \\"data.description\\", weight: 0.7 },\\n { name: \\"data.url\\", weight: 0.3 },\\n ],\\n });\\n\\n const results = await fuse.search(query);\\n return results;\\n },\\n }),\\n};
\\n\\n\\n\\nIn this case, we create the Fuse instance with a few options. We give it a threshold value between 0 and 1 to decide how “fuzzy” to make the search. Fuzziness is definitely something that depends on use case and the dataset. In our dataset, I’ve found 0.3
to be a great threshold.
The keys
array allows you to specify which data should be searched. In this case, I want all the data to be searched, but I want to allow for different weighting for each item. The title should be most important, followed by the description, and the URL should be last. This way, I can search for keywords in all these areas.
Once there’s a new Fuse instance, we run fuse.search(query)
to have Fuse check the data, and return an array of results.
When we run this with our front-end, we find we have one more issue to tackle.
\\n\\n\\n\\nThe structure of the data returned is not quite what it was with our simple JavaScript. Each result now has a refIndex
and an item
. All our data lives on the item, so we need to destructure the item off of each returned result.
To do that, adjust the front-end forEach
.
// create a div for each search result\\ndata.forEach(({ item }) => {\\n const div = document.createElement(\\"div\\");\\n div.innerHTML = `\\n <a href=\\"${item.data?.url}\\" class=\\"block p-6 bg-white border border-gray-200 rounded-lg shadow-sm hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-700 dark:hover:bg-gray-700\\">\\n <h3 class=\\"mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white\\">\\n ${item.data?.pageTitle}\\n </h3>\\n <p class=\\"font-normal text-gray-700 dark:text-gray-400\\">\\n ${item.data?.description}\\n </p>\\n </a>`;\\n // append the div to the results container\\n results.appendChild(div);\\n});
\\n\\n\\n\\nNow, we have a fully working search for our bookmarks.
\\n\\n\\n\\nThis just scratches the surface of what you can do with Astro Actions. For instance, we should probably add additional error handling based on the error we get back. You can also experiment with handling this at the page-level and letting there be a Search page where the Action is used as a form action and handles it all as a server request instead of with front-end JavaScript code. You could also refactor the JavaScript from the admittedly low-tech vanilla JS to something a bit more robust with React, Svelte, or Vue.
\\n\\n\\n\\nOne thing is for sure, Astro keeps looking at the front-end landscape and learning from the mistakes and best practices of all the other frameworks. Actions, Content Layer, and more are just the beginning for a truly compelling front-end framework.
\\nPowering Search With Astro Actions and Fuse.js originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
","description":"Static sites are wonderful. I’m a big fan. They also have their issues. Namely, static sites either are purely static or the frameworks that generate them completely lose out on true static generation when you just dip your toes in the direction of server routes.\\n\\nAstro has…","guid":"https://css-tricks.com/?p=384941","author":"Bryan Robinson","authorUrl":null,"authorAvatar":null,"insertedAt":"2025-03-11T16:23:35.731Z","publishedAt":"2025-03-11T15:26:10.040Z","media":[{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-1.png?resize=2796%2C1060&ssl=1","type":"photo","width":2796,"height":1060},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-2.png?resize=1874%2C732&ssl=1","type":"photo","width":1874,"height":732},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-3.png?resize=2010%2C1554&ssl=1","type":"photo","width":2010,"height":1554},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-4.png?resize=1816%2C1388&ssl=1","type":"photo","width":1816,"height":1388},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-5.png?resize=1832%2C898&ssl=1","type":"photo","width":1832,"height":898},{"url":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2025/02/image-6.png?resize=1658%2C1600&ssl=1","type":"photo","width":1658,"height":1600}],"categories":["Articles","astro","static site generators"],"attachments":null,"extra":null,"language":null,"feeds":{"type":"feed","id":"41719081557593116","url":"https://css-tricks.com/feed/","title":"CSS-Tricks","description":"Tips, Tricks, and Techniques on using Cascading Style Sheets.","siteUrl":"https://css-tricks.com/","image":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1","errorMessage":null,"errorAt":null,"ownerUserId":null}},{"feedId":"42579624844251149","id":"122351868489391104","title":"How To Build Confidence In Your UX Work","url":"https://smashingmagazine.com/2025/03/how-to-build-confidence-in-your-ux-work/","content":"When I start any UX project, typically, there is very little confidence in the successful outcome of my UX initiatives. In fact, there is quite a lot of reluctance and hesitation, especially from teams that have been burnt by empty promises and poor delivery in the past.
\\n\\nGood UX has a huge impact on business. But often, we need to build up confidence in our upcoming UX projects. For me, an effective way to do that is to address critical bottlenecks and uncover hidden deficiencies — the ones that affect the people I’ll be working with.
\\n\\nLet’s take a closer look at what this can look like.
\\n\\n\\n\\nThis article is part of our ongoing series on UX. You can find more details on design patterns and UX strategy in Smart Interface Design Patterns 🍣 — with live UX training coming up soon. Free preview.
\\n\\nUX Doesn’t Disrupt, It Solves Problems\\n\\nBottlenecks are usually the most disruptive part of any company. Almost every team, every unit, and every department has one. It’s often well-known by employees as they complain about it, but it rarely finds its way to senior management as they are detached from daily operations.
\\n\\nThe bottleneck can be the only senior developer on the team, a broken legacy tool, or a confusing flow that throws errors left and right — there’s always a bottleneck, and it’s usually the reason for long waiting times, delayed delivery, and cutting corners in all the wrong places.
\\n\\nWe might not be able to fix the bottleneck. But for a smooth flow of work, we need to ensure that non-constraint resources don’t produce more than the constraint can handle. All processes and initiatives must be aligned to support and maximize the efficiency of the constraint.
\\n\\nSo before doing any UX work, look out for things that slow down the organization. Show that it’s not UX work that disrupts work, but it’s internal disruptions that UX can help with. And once you’ve delivered even a tiny bit of value, you might be surprised how quickly people will want to see more of what you have in store for them.
\\n\\nThe Work Is Never Just “The Work”\\n\\nMeetings, reviews, experimentation, pitching, deployment, support, updates, fixes — unplanned work blocks other work from being completed. Exposing the root causes of unplanned work and finding critical bottlenecks that slow down delivery is not only the first step we need to take when we want to improve existing workflows, but it is also a good starting point for showing the value of UX.
\\n\\nTo learn more about the points that create friction in people’s day-to-day work, set up 1:1s with the team and ask them what slows them down. Find a problem that affects everyone. Perhaps too much work in progress results in late delivery and low quality? Or lengthy meetings stealing precious time?
\\n\\nOne frequently overlooked detail is that we can’t manage work that is invisible. That’s why it is so important that we visualize the work first. Once we know the bottleneck, we can suggest ways to improve it. It could be to introduce 20% idle times if the workload is too high, for example, or to make meetings slightly shorter to make room for other work.
\\n\\nThe Theory Of Constraints\\n\\nThe idea that the work is never just “the work” is deeply connected to the Theory of Constraints discovered by Dr. Eliyahu M. Goldratt. It showed that any improvements made anywhere beside the bottleneck are an illusion.
\\nAny improvement after the bottleneck is useless because it will always remain starved, waiting for work from the bottleneck. And any improvements made before the bottleneck result in more work piling up at the bottleneck.
\\n\\nTo improve flow, sometimes we need to freeze the work and bring focus to one single project. Just as important as throttling the release of work is managing the handoffs. The wait time for a given resource is the percentage of time that the resource is busy divided by the percentage of time it’s idle. If a resource is 50% utilized, the wait time is 50/50, or 1 unit.
\\n\\nIf the resource is 90% utilized, the wait time is 90/10, or 9 times longer. And if it’s 99% of time utilized, it’s 99/1, so 99 times longer than if that resource is 50% utilized. The critical part is to make wait times visible so you know when your work spends days sitting in someone’s queue.
\\n\\nThe exact times don’t matter, but if a resource is busy 99% of the time, the wait time will explode.
\\n\\nOur goal is to maximize flow: that means exploiting the constraint but creating idle times for non-constraint to optimize system performance.
\\n\\nOne surprising finding for me was that any attempt to maximize the utilization of all resources — 100% occupation across all departments — can actually be counterproductive. As Goldratt noted, “An hour lost at a bottleneck is an hour out of the entire system. An hour saved at a non-bottleneck is worthless.”
\\n\\nI can only wholeheartedly recommend The Phoenix Project, an absolutely incredible book that goes into all the fine details of the Theory of Constraints described above.
\\n\\nIt’s not a design book but a great book for designers who want to be more strategic about their work. It’s a delightful and very real read about the struggles of shipping (albeit on a more technical side).
\\n\\nWrapping Up\\n\\nPeople don’t like sudden changes and uncertainty, and UX work often disrupts their usual ways of working. Unsurprisingly, most people tend to block it by default. So before we introduce big changes, we need to get their support for our UX initiatives.
\\n\\nWe need to build confidence and show them the value that UX work can have — for their day-to-day work. To achieve that, we can work together with them. Listening to the pain points they encounter in their workflows, to the things that slow them down.
\\n\\nOnce we’ve uncovered internal disruptions, we can tackle these critical bottlenecks and suggest steps to make existing workflows more efficient. That’s the foundation to gaining their trust and showing them that UX work doesn’t disrupt but that it’s here to solve problems.
\\n\\nNew: How To Measure UX And Design Impact\\n\\nMeet Measure UX & Design Impact (8h), a practical guide for designers and UX leads to measure and show your UX impact on business. Watch the free preview or jump to the details.
\\n\\n\\n \\n25 video lessons (8h) + Live UX Training.
100 days money-back-guarantee.
25 video lessons (8h). Updated yearly.
Also available as a UX Bundle with 2 video courses.
The videos from Smashing Magazine’s recent event on accessibility were just posted the other day. I was invited to host the panel discussion with the speakers, including a couple of personal heroes of mine, Stéphanie Walter and Sarah Fossheim. But I was just as stoked to meet Kardo Ayoub who shared his deeply personal story as a designer with a major visual impairment.
\\n\\n\\n\\n\\n\\n\\n\\nI’ll drop the video here:
\\n\\n\\n\\n\\n\\n\\n\\nI’ll be the first to admit that I had to hold back my emotions as Kardo detailed what led to his impairment, the shock that came of it, and how he has beaten the odds to not only be an effective designer, but a real leader in the industry. It’s well worth watching his full presentation, which is also available on YouTube alongside the full presentations from Stéphanie and Sarah.
\\nSmashing Meets Accessibility originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.
","description":"The videos from Smashing Magazine’s recent event on accessibility were just posted the other day. I was invited to host the panel discussion with the speakers, including a couple of personal heroes of mine, Stéphanie Walter and Sarah Fossheim. But I was just as stoked to meet Kar…","guid":"https://css-tricks.com/?p=385112","author":"Geoff Graham","authorUrl":null,"authorAvatar":null,"insertedAt":"2025-03-10T16:49:23.334Z","publishedAt":"2025-03-10T15:08:47.613Z","media":null,"categories":["Links","accessibility"],"attachments":null,"extra":null,"language":null,"feeds":{"type":"feed","id":"41719081557593116","url":"https://css-tricks.com/feed/","title":"CSS-Tricks","description":"Tips, Tricks, and Techniques on using Cascading Style Sheets.","siteUrl":"https://css-tricks.com/","image":"https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1","errorMessage":null,"errorAt":null,"ownerUserId":null}},{"feedId":"41147805272531984","id":"121571572829627392","title":"CSS @supports规则又新增font-tech,font-format判断","url":"https://www.zhangxinxu.com/wordpress/2025/03/css-supports-font-tech-format/","content":"by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11531
\\n本文可全文转载,独立域名个人网站无需授权,但需要保留原作者、出处以及文中链接,任何网站均可摘要聚合,商用请联系授权。
继续关注Web前端前沿新特性,这次快速介绍CSS @supports规则新增加的两个特性判断,一个是font-tech()
,另外一个是font-format()
函数,都来看看是什么作用吧。
font-tech()
函数可以检查浏览器是否支持用于布局和渲染的指定字体技术。
例如,下面这段CSS代码可以判断浏览器是否支持COLRv1字体(一种彩色字体技术)技术。
\\n@supports font-tech(color-COLRv1) {}\\n
下表展示了可以作为font-tech()
函数的参数,数据源自MDN文档。
5年前的这篇文章“关于CSS emoji字体和OpenType-SVG我所知道的一些事”对其中一些字体技术有过简单的介绍,有兴趣的可以了解下。
\\n技术 | \\n描述 | \\n
---|---|
彩色字体技术 | \\n\\n |
color-colrv0 | \\nCOLR 版本 0 彩色字体 | \\n
color-colrv1 | \\nCOLR 版本 1 彩色字体 | \\n
color-svg | \\nSVG 彩色字体 | \\n
color-sbix | \\n标准位图图形字体 | \\n
color-cbdt | \\n彩色位图数据 | \\n
字体特征技术 | \\n\\n |
features-opentype | \\nOpenType GSUB 以及 GPOS 技术 | \\n
features-aat | \\nTrueType morx 以及 kerx 技术 | \\n
features-graphite | \\n石墨特征,即 Silf , Glat , Gloc , Feat 以及 Sill\\n |
其他字体技术值 | \\n\\n |
incremental-patch | \\n使用补丁子集方法进行增量字体加载 | \\n
incremental-range | \\n使用范围请求方法增量加载字体 | \\n
incremental-auto | \\n使用自动协商方法的增量字体加载 | \\n
variations | \\nTrueType和OpenType字体中的字体变化,用于控制字体轴、粗细、字形等。 | \\n
palettes | \\n字体调色板技术,也就是通过font-palttle选择字体中的许多调色板之一 | \\n
font-format()
这个比较好理解,是检测浏览器是否支持制定的字体格式的。
同样支持的参数值和对应的字体格式如下表所示:
\\n格式 | \\n描述 | \\n文件后缀 | \\n
---|---|---|
collection | \\nOpenType集合 | \\n.otc , .ttc | \\n
embedded-opentype | \\n嵌入式OpenType | \\n.eot | \\n
opentype | \\nOpenType | \\n.ttf , .otf | \\n
svg | \\nSVG字体 (已废弃) | \\n.svg , .svgz | \\n
truetype | \\nTrueType | \\n.ttf | \\n
woff | \\nWOFF 1.0 (Web Open Font Format) | \\n.woff | \\n
woff2 | \\nWOFF 2.0 (Web Open Font Format) | \\n.woff2 | \\n
使用示意:
\\n@supports font-format(woff2) {\\n /* 浏览器支持woff2字体 */\\n}\\n
好,这两个字体相关的检测函数算是介绍完了,下面过来点评下。
\\n直接说结论:没用的废物特性!
\\n但两者的废物之处各有不同。
\\nfont-tech()
是检测字体技术的,这个对于中文场景就是鸡肋特性,因为中文字体是不会使用这类技术的,无他,成本太高。
英文字体就一些字母和数字,你里面搞彩色字体,变体之类的,成本是可控的。
\\n但是中文字体,少说几千个,里面再造字体特征,变形之类的,是多么浩大的工程,所以,目前业界,至少到现在为止,我是没有看到一块有彩色字体或者字体特征的中文字体的,日文字体倒是有。
\\n而font-format()
函数的问题在于出现的太晚了。
例如woff2字体的检测,这个所有现代浏览器都已经支持了,还有检测的必要吗,没了,没有意义了,就好像都已经射完了,才从抽屉里翻出一个套。
\\n所以啊,大家看看,了解下有这么个东西就好了。
\\nCSS这门语言很神奇,就是很多CSS属性大行其道并不是因为其原本的设计初衷,而是其衍生的特性。
\\n所以,这里这两个函数,如果抛开其原本作用,倒不是没有价值可言。
\\n例如,我们可以利用此函数判断当前浏览器是不是Safari浏览器,例如color-colrv1彩色字体技术Safari是不支持的,我们可以借此判断当前浏览器。
\\n@supports not font-tech(color-COLRv1) {\\n /* Safari浏览器 */\\n}\\n
兼容性截图示意:
\\n是不是还挺有趣的?
\\n好,就扯这么多。
\\n🥱😴🤤
\\n本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
\\n本文地址:https://www.zhangxinxu.com/wordpress/?p=11531
(本篇完)
","description":"by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11531 本文可全文转载,独立域名个人网站无需授权,但需要保留原作者、出处以及文中链接,任何网站均可摘要聚合,商用请联系授权。\\n\\n继续关注Web前端前沿新特性,这次快速介绍CSS @supports规则新增加的两个特性判断,一个是font-tech(),另外一个是font-format()函数,都来看看是什么作用吧。\\n\\n一、font-tech()的功能\\n\\nfont-tech()函数可以检查浏览器是否支持用于布局和渲染的指定字体技术。\\n\\n例如…","guid":"https://www.zhangxinxu.com/wordpress/?p=11531","author":"张 鑫旭","authorUrl":null,"authorAvatar":null,"insertedAt":"2025-03-09T14:45:36.635Z","publishedAt":"2025-03-09T13:11:08.438Z","media":[{"url":"https://image.zhangxinxu.com/image/blog/202503/font-some-cover.jpg","type":"photo","width":320,"height":204},{"url":"https://image.zhangxinxu.com/image/blog/202503/2025-03-09_210039.png","type":"photo","width":628,"height":291}],"categories":["CSS相关","@font-palette-values","@supports","font-feature-settings","font-plattle","Safari"],"attachments":null,"extra":null,"language":null,"feeds":{"type":"feed","id":"41147805272531984","url":"https://www.zhangxinxu.com/wordpress/feed/","title":"张鑫旭-鑫空间-鑫生活","description":"it\'s my whole life!","siteUrl":"https://www.zhangxinxu.com/wordpress","image":null,"errorMessage":null,"errorAt":null,"ownerUserId":null}}]}')