Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 36 additions & 193 deletions blog/public/blog/welcome-to-the-xmlui-blog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
In this inaugural post we'll explore the development of the blog engine we're using on this site. Our tagline is *Practical User Interfaces Built Simply* and creating this blog couldn't have been simpler. The whole site, of which the blog is now a part, is an XMLUI app built with components including [NavPanel](https://docs.xmlui.org/components/NavPanel), [NavLink](https://docs.xmlui.org/components/NavLink), [Pages](https://docs.xmlui.org/components/Pages), [Page](https://docs.xmlui.org/components/Page), and [Markdown](https://docs.xmlui.org/components/Markdown).

Let's see how it evolved.
In this inaugural post we'll explore the development of the blog engine we're using on this site. Our tagline is *Practical User Interfaces Built Simply* and creating this blog couldn't have been simpler. It's an XMLUI app built with a handful of core components (including [NavPanel](https://docs.xmlui.org/components/NavPanel), [NavLink](https://docs.xmlui.org/components/NavLink), [Pages](https://docs.xmlui.org/components/Pages), [Page](https://docs.xmlui.org/components/Page), and [Markdown](https://docs.xmlui.org/components/Markdown)) and a couple of [user-defined components](https://docs.xmlui.org/user-defined-components).

## The simplest possible thing

Expand Down Expand Up @@ -50,7 +48,7 @@ We started with the simplest possible approach: post metadata and data as litera

You can use it right here or you can click the ![](/resources/pg-popout.svg) icon to open a playground where you can make live changes.

This is a pretty good start! We can write posts, arrange them in reverse chronological order, and hey, it's the essence of a blog. Since it's a blog about XMLUI the live playground is a nice bonus that any XMLUI app might put to good use. The user interfaces that you build with XMLUI will require some explaining, it's handy to explain with working examples as well as images, text, and video.
This is a pretty good start! We can write posts, arrange them in reverse chronological order, and hey, it's the essence of a blog. The live playground is a nice bonus that any XMLUI app might put to good use. The user interfaces that you build with XMLUI will require some explaining, it's handy to explain with working examples as well as images, text, and video.

Let's unpack how this works, there isn't much to it. The `App` declared in `Main.xmlui` sets up navigation.

Expand All @@ -66,9 +64,9 @@ Let's unpack how this works, there isn't much to it. The `App` declared in `Main
</App>
```

Each Page contains a [user-defined component](https://docs.xmlui.org/user-defined-components) called `BlogPage`. In the prototype, the `BlogPage` component receives page content as `$props.content`.
Each Page contains a user-defined component called `BlogPage` that receives the properties `content`, `title`, `author`, and `date`.

```xmlui copy {4}
```xmlui
<Pages>
<Page url="/newest-post">
<BlogPage
Expand All @@ -80,21 +78,16 @@ Each Page contains a [user-defined component](https://docs.xmlui.org/user-define
</Pages>
```

Here's how the prototype `BlogPage` assembles data and metadata to create a post.
Here's how `BlogPage` assembles data and metadata to create a post.

```xmlui copy {4, 6, 8, 11}
```xmlui /$props/
<Component name="BlogPage">
<VStack width="85%">
<VStack>
<H1>{$props.title}</H1>
<HStack gap="$space-2">
<Text>{$props.date}</Text>
<Text> - </Text>
<Text>{$props.author}</Text>
</HStack>
<VStack gap="0">
<H1>{$props.post.title}</H1>
<Text>{$props.post.date} • {$props.post.author}</Text>
</VStack>
<Markdown content="{$props.content}" />
</VStack>
<Image src="/blog/images/{$props.post.image}" />
<Markdown marginTop="$space-4" data="/blog/{$props.post.slug}.md" />
</Component>
```

Expand Down Expand Up @@ -192,7 +185,7 @@ The `NavLink` uses the post's slug to bind to its corresponding `Page`.
</Page>
```

And the `Page` passes the complete post object to `BlogPage`. In v1 we used the `content` property of the `Markdown` component to pass a string. In v2 we use the `data` property to pass an URL constructed from the post slug.
And the `Page` passes the complete post object to `BlogPage`. In v1 we used the `content` property of the `Markdown` component to pass a string. In v2 we use the `data` property to pass a URL constructed from the post slug.

```xmlui copy {11}
<Component name="BlogPage">
Expand All @@ -216,31 +209,32 @@ Although it's feasible to use a `NavGroup` to list the posts, a blog should real

```xmlui copy
<Component name="BlogOverview">
<Stack width="85%">
<H1>XMLUI Blog</H1>
<Text>Latest updates, tutorials, and insights for building with XMLUI</Text>
</Stack>
<List
data="{
<CVStack>
<VStack width="100%">
<H1>XMLUI Blog</H1>
<Text>Latest updates, tutorials, and insights for building with XMLUI</Text>
<List data="{
$props.posts.sort(function(a, b) {
return new Date(b.date) - new Date(a.date);
})
}">
<VStack gap="$space-2" width="90%">
<Link to="/blog/{$item.slug}">
<Text fontSize="larger">
{$item.title}
</Text>
</Link>
<Text>
{$item.date} • {$item.author}
</Text>
<Link to="/blog/{$item.slug}">
<Image src="/blog/images/{$item.image}" />
</Link>
<Stack height="3rem" />
<VStack gap="$space-2">
<Link to="/blog/{$item.slug}">
<Text fontSize="larger">
{$item.title}
</Text>
</Link>
<Text>
{$item.date} • {$item.author}
</Text>
<Link to="/blog/{$item.slug}">
<Image src="/blog/images/{$item.image}" />
</Link>
<Stack height="$space-8" />
</VStack>
</List>
</VStack>
</List>
</CVStack>
</Component>
```

Expand Down Expand Up @@ -271,7 +265,8 @@ We can't call it a blog unless it provides an RSS feed. For that we've added a s

## Deploy standalone

Suppose you wanted to decouple the blog engine from the monorepo and use it standalone? Let's start with this footprint.
Our blog lives in the XMLUI monorepo where it coordinates with the landing page and docs.
But it can exist standalone, you only need a folder with a handful of files.


```
Expand Down Expand Up @@ -303,168 +298,16 @@ Here's the `index.html`.
<title>XMLUI blog test</title>
<script src="xmlui/xmlui-0.10.8.js"></script>
<script src="xmlui/xmlui-playground.js"></script>
<script>
</script>
</head>
<body>
</body>
</html>
```

And here's `Main.xmlui`.

```xmlui copy
<Fragment>
<App
layout="vertical-full-header"
noScrollbarGutters="false"
when="{!window.location.hash.includes('/playground')}"
var.posts = `{[
{
title: "Welcome to the XMLUI blog!",
slug: "welcome-to-the-xmlui-blog",
author: "Jon Udell",
date: "2025-09-01",
image: "blog-scrabble.png"
},
{
title: "Lorem Ipsum!",
slug: "lorem-ipsum",
author: "H. Rackham",
date: "1914-06-03",
image: "lorem-ipsum.png"
}
]}`
>
<AppHeader>
<property name="logoTemplate">
<Link to="https://xmlui.org/">
<Logo height="$space-8" />
</Link>
</property>
<SpaceFiller />
<Search
data="{appGlobals.plainTextContent}"
when="{mediaSize.sizeIndex > 2}" />
<ToneSwitch />
</AppHeader>
<NavPanel when="{ mediaSize.sizeIndex > 2 && (searchState.value.searchResults.length === 0 || !searchState.value.searchQuery)}">
<Stack
paddingHorizontal="$space-4"
paddingBottom="$space-4"
when="{mediaSize.sizeIndex <= 2}">
<Search data="{appGlobals.plainTextContent}" />
</Stack>
<NavGroup label="Recent posts" to="/blog" >
<NavLink label="Welcome to the XMLUI blog!" to="/blog/{posts[0].slug}" />
<NavLink label="Loreum ipsum" to="/blog/{posts[1].slug}" />
</NavGroup>
</NavPanel>
<Pages fallbackPath="/404">
<Page url="/blog">
<BlogOverview posts="{posts}" />
</Page>
<Page url="/blog/{posts[0].slug}">
<BlogPage post="{posts[0]}" />
</Page>
<Page url="/blog/{posts[1].slug}">
<BlogPage post="{posts[1]}" />
</Page>
<Page url="/404">
<PageNotFound />
</Page>
</Pages>
<Footer>
<CHStack width="*">
This site is an XMLUI™ app.
<SpaceFiller />
<Link
to="https://github.com/xmlui-org/xmlui/tree/main/docs/src"
target="_blank">
<Button variant="ghost" icon="github" />
</Link>
</CHStack>
</Footer>
</App>
<StandalonePlayground when="{window.location.hash.includes('/playground')}" />
</Fragment>
```

That's all we need to serve the blog. Note that we include `xmlui-playground.js`. The live playgrounds you can use here are provided by an extension, and a standalone app can use that extension in the same way our main site does. So when you serve the blog from a static webserver, the playground examples work the same way.

You can host the standalone blog on any static webserver. We'll do that, but first let's create a search mechanism that's decoupled from the monorepo's build and works entirely client-side. We'll create two user-defined components: `SearchPrep` and `BlogSearch`.

```xmlui copy
<Page url="/search">
<SearchPrep posts="{posts}" />
<BlogSearch posts="{posts}" searchIndex="{window.getblogPosts()}" />
</Page>
```

Here is `SearchPrep`. It uses `DataSource` in a `List` to read posts, and calls global functions to process them.

```xmlui copy
<Component name="SearchPrep" var.count="{0}">

<List when="{count <= $props.posts.length}" data="{$props.posts}">
<DataSource
url="/blog/{$item.slug}.md"
onLoaded="(data) => {
// Set indexing state on first load;
if (window.blogIsIndexing === '') {
window.setBlogIndexing();
}

window.setBlogSearchEntry('/blog/' + $item.slug, $item.title + '\n' + data);
console.log('Added to blogPosts:', '/blog/' + $item.slug);
count++;

if (window.getBlogSearchCount() >= $props.posts.length) {
window.stopBlogIndexing();
console.log('Blog indexing complete!');

}
}" />

</List>

</Component>
```

Here is `BlogSearch`. It provides a reactive search box so as you type, another global function finds fragments in posts that match your current query.

```xmlui copy
<Component name="BlogSearch">
<VStack gap="$space-4">
<TextBox
id="searchQuery"
placeholder="Search blog posts..."
width="15rem"
/>
<List data="{
window.searchBlogPosts($props.posts, $props.searchIndex, searchQuery.value)
}">
<VStack gap="$space-2">
<Link to="/blog/{$item.post.slug}">
<H2 value="{$item.post.title}" />
</Link>
<List data="{$item.matches}">
<Card>
<Text backgroundColor="$color-warn-100" value="{$item.context}" />
</Card>
</List>
</VStack>
</List>
</VStack>
</Component>
```

With these ingredients in place, I dragged the folder containing the standalone app onto Netlify's drop target. Check it out!
I dragged the folder containing the standalone app onto Netlify's drop target. Check it out!

[https://test-xmlui-blog.netlify.app/](https://test-xmlui-blog.netlify.app/)

The post you are reading here was deployed in a similar way.

## XMLUI for publishing

We get it, blog engines are a dime a dozen. We made this one because XMLUI was already a strong publishing system that we use for the [docs](https://docs.xmlui.org), [demo](https://demo.xmlui.org), and [landing page](https://xmlui.org). The `Markdown` component, with its support for playgrounds, works really well and it made sense to leverage that for our blog. We're not saying that you *should* build a blog engine with XMLUI but it's clearly something you *could* do. We think it's pretty easy to create a competent engine that makes life easy for authors and readers.
Expand Down
59 changes: 29 additions & 30 deletions blog/src/Main.xmlui
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
<App
layout="vertical-full-header"
noScrollbarGutters="false"
var.posts = `{[
layout="vertical"
var.showNavPanelThreshold="{6}"
var.posts=`{[
{
title: "Welcome to the XMLUI blog!",
slug: "welcome-to-the-xmlui-blog",
author: "Jon Udell",
date: "2025-09-10",
date: "2025-10-20",
image: "blog-scrabble.png"
}
]}`
>
]}`>
<AppState
id="navState"
initialValue="{{
showNavPanel: showNav(),
showNavPanelThreshold: showNavPanelThreshold,
posts: posts.length
}}" />
<AppHeader>
<property name="logoTemplate">
<Link to="https://xmlui.org/">
<Logo height="$space-8" />
<CHStack height="$space-6">
<Logo />
<Text variant="em">Tutorials, tips, best practices</Text>
</CHStack>
</property>
<property name="profileMenuTemplate">
<SpaceFiller />
<Link when="{!['/', '/blog'].includes($pathname)}" to="/">
<Button height="$space-6" padding="$space-2" label="All Posts" />
</Link>
</property>
<SpaceFiller />
<Search
data="{appGlobals.plainTextContent}"
when="{mediaSize.sizeIndex > 2}" />
<ToneSwitch />
</AppHeader>
<NavPanel
when="{!window.location.hash.includes('/404') || mediaSize.sizeIndex <= 2}">
<Stack
paddingHorizontal="$space-4"
paddingBottom="$space-4"
when="{mediaSize.sizeIndex <= 2}">
<Search data="{appGlobals.plainTextContent}" />
</Stack>
<NavGroup label="Recent posts" to="/blog" >
<NavLink label="Welcome to the XMLUI blog!" to="/blog/{posts[0].slug}" />
<NavPanel>
<NavGroup label="Recent posts" to="/blog">
<NavLink
label="Welcome to the XMLUI blog!"
to="/blog/{posts[0].slug}" />
</NavGroup>
</NavPanel>
<Pages fallbackPath="/404">
Expand All @@ -50,14 +53,10 @@
</Page>
</Pages>
<Footer>
<CHStack width="*">
<CHStack width="100%">
This site is an XMLUI™ app.
<SpaceFiller />
<Link
to="https://github.com/xmlui-org/xmlui/tree/main/docs/src"
target="_blank">
<Button variant="ghost" icon="github" />
</Link>
<ToneSwitch />
</CHStack>
</Footer>
</App>
</App>
3 changes: 3 additions & 0 deletions blog/src/Main.xmlui.xs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function showNav() {
return posts.length > showNavPanelThreshold && mediaSize.sizeIndex > 2;
}
Loading