This is a viewer only at the moment see the article on how this works.
To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk
This is a preview from the server running through my markdig pipeline
Monday, 10 November 2025
Au cours des derniers jours, j'ai été en train de marteler à la nouvelle barre de filtre de blog: la sélection de la langue, la commande, et un sélectionneur de gamme de dates qui essaie très dur d'être intelligent (parfois trop intelligent).
Ce post marche à travers ce que j'ai construit, pourquoi quelques choses sont allées de côté (dates sans se comporter, met en évidence pas rafraîchissant), et comment je les ai réparés.
Rappelez-vous que ce site est un travail en cours, ce genre de choses va se produire lorsque vous mangez-votre-dogfood!
/blog/date-rangeÉtendue de la date<clear-param>Effacer l'aide de la balise param: Moyen facile de nettoyer les filtres à l'aide
En chemin, j'ai eu quelques bugs : les plages de dates étant perdues sur le changement de langue/commande, les temps forts du calendrier ne rafraîchissant pas sur l'échange, et un moment amusant où les styles de mode sombre ont fait le calendrier cassé parce que Flatpickr n'était pas redessiner.
<div id="filters" hx-target="#content">
<select id="languageSelect">…</select>
<select id="orderSelect">
<option value="date_desc">Newest first</option>
<option value="date_asc">Oldest first</option>
<option value="title_asc">Title A–Z</option>
<option value="title_desc">Title Z–A</option>
</select>
<input id="dateRange" type="text" placeholder="YYYY-MM-DD → YYYY-MM-DD" />
<button id="clearDateFilter">Clear</button>
<div id="filterSummary"></div>
</div>
<div id="content"><!-- HTMX swaps blog list here --></div>
C'est la structure brute que le script attend:Mostlylucid/src/js/blog-index.jsClient: HTMX + câblage Flatpickr
function applyNavigation(u){
const target = document.querySelector('#content');
try{ window.history.pushState({}, '', u.toString()); }catch{}
if(window.htmx && target){
window.htmx.ajax('GET', u.toString(), {
target: '#content',
swap: 'outerHTML show:none',
headers: {'pagerequest': 'true'}
});
} else {
window.location.href = u.toString();
}
}
const url = new URL(window.location.href);
const existingStart = url.searchParams.get('startDate');
const existingEnd = url.searchParams.get('endDate');
const existingLang = url.searchParams.get('language') || 'en';
const existingOrderBy = (url.searchParams.get('orderBy') || 'date').toLowerCase();
const existingOrderDir = (url.searchParams.get('orderDir') || 'desc').toLowerCase();
langSelect.value = existingLang;
orderSelect.value = `${existingOrderBy}_${existingOrderDir}`;
updateSummary();
async function fetchMonth(year, month, language){
const res = await fetch(`/blog/calendar-days?year=${year}&month=${month}&language=${encodeURIComponent(language||'en')}`);
if(!res.ok) return new Set();
const j = await res.json();
return new Set(j.dates || []);
}
function formatYMD(d){ return d.toISOString().substring(0,10); }
let highlightDates = new Set();
const fp = window.flatpickr(input, {
mode: 'range',
dateFormat: 'Y-m-d',
defaultDate: [existingStart, existingEnd].filter(Boolean),
onDayCreate: function(_dObj,_dStr,fpInstance,dayElem){
const ymd = formatYMD(dayElem.dateObj);
if(highlightDates.has(ymd)){
dayElem.classList.add('has-post');
dayElem.style.background = 'rgba(76,175,80,0.35)';
dayElem.style.borderRadius = '6px';
}
},
onMonthChange: async function(_sd,_ds,fpInstance){
highlightDates = await fetchMonth(fpInstance.currentYear, fpInstance.currentMonth+1, langSelect.value);
fpInstance.redraw();
},
onOpen: async function(_sd,_ds,fpInstance){
highlightDates = await fetchMonth(fpInstance.currentYear, fpInstance.currentMonth+1, langSelect.value);
fpInstance.redraw();
},
onChange: function(selectedDates){
if(selectedDates.length === 2){
const [start,end] = selectedDates;
const u = new URL(window.location.href);
u.searchParams.set('startDate', formatYMD(start));
u.searchParams.set('endDate', formatYMD(end));
u.searchParams.set('page','1');
u.searchParams.set('language', langSelect.value);
const [ob,od] = orderSelect.value.split('_');
u.searchParams.set('orderBy', ob);
u.searchParams.set('orderDir', od);
updateSummary();
applyNavigation(u);
}
}
});
langSelect.addEventListener('change', async function(){
const u = new URL(window.location.href);
u.searchParams.set('language', langSelect.value);
u.searchParams.set('page', '1');
const [ob,od] = (orderSelect.value||'date_desc').split('_');
u.searchParams.set('orderBy', ob);
u.searchParams.set('orderDir', od);
if(input._flatpickr && input._flatpickr.selectedDates.length===2){
const [s,e] = input._flatpickr.selectedDates;
u.searchParams.set('startDate', formatYMD(s));
u.searchParams.set('endDate', formatYMD(e));
}
// refresh calendar highlights for new language
if(input._flatpickr){
const fp = input._flatpickr;
highlightDates = await fetchMonth(fp.currentYear, fp.currentMonth+1, langSelect.value);
fp.redraw();
}
updateSummary();
applyNavigation(u);
});
const obs = new MutationObserver(() => {
const dr = root.querySelector('#dateRange');
const fp = dr && dr._flatpickr;
if(fp && typeof fp.redraw === 'function') fp.redraw();
});
obs.observe(document.documentElement, { attributes:true, attributeFilter:['class'] });
Préserver les dates lors du changement de langue ou de la commande
calendar-daysTrois paramètres alimentent la page :date-rangeA// GET /blog
[HttpGet]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request",
VaryByQueryKeys = new[] { "page", "pageSize", nameof(startDate), nameof(endDate), nameof(language), nameof(orderBy), nameof(orderDir) },
Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 3600, VaryByHeaderNames = new[] { "hx-request" },
VaryByQueryKeys = new[] { nameof(page), nameof(pageSize), nameof(startDate), nameof(endDate), nameof(language), nameof(orderBy), nameof(orderDir) })]
public async Task<IActionResult> Index(int page = 1, int pageSize = 20, DateTime? startDate = null, DateTime? endDate = null,
string language = MarkdownBaseService.EnglishLanguage, string orderBy = "date", string orderDir = "desc")
{
var posts = await blogViewService.GetPagedPosts(page, pageSize, language: language, startDate: startDate, endDate: endDate);
posts.LinkUrl = Url.Action("Index", "Blog", new { startDate, endDate, language, orderBy, orderDir });
if (Request.IsHtmx()) return PartialView("_BlogSummaryList", posts);
return View("Index", posts);
}
// GET /blog/calendar-days?year=2025&month=11&language=en
[HttpGet("calendar-days")]
[ResponseCache(Duration = 300, VaryByHeader = "hx-request", VaryByQueryKeys = new[] { nameof(year), nameof(month), nameof(language) }, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 1800, VaryByHeaderNames = new[] { "hx-request" }, VaryByQueryKeys = new[] { nameof(year), nameof(month), nameof(language) })]
public async Task<IActionResult> CalendarDays(int year, int month, string language = MarkdownBaseService.EnglishLanguage)
{
if (year < 2000 || month < 1 || month > 12) return BadRequest("Invalid year or month");
var start = new DateTime(year, month, 1);
var end = start.AddMonths(1).AddDays(-1);
var posts = await blogViewService.GetPostsForRange(start, end, language: language);
if (posts is null) return Json("");
var dates = posts.Select(p => p.PublishedDate.Date).Distinct().OrderBy(d => d).Select(d => d.ToString("yyyy-MM-dd")).ToList();
return Json(new { dates });
}
// GET /blog/date-range?language=en
[HttpGet("date-range")]
[ResponseCache(Duration = 3600, VaryByHeader = "hx-request", VaryByQueryKeys = new[] { nameof(language) }, Location = ResponseCacheLocation.Any)]
[OutputCache(Duration = 7200, VaryByHeaderNames = new[] { "hx-request" }, VaryByQueryKeys = new[] { nameof(language) })]
public async Task<IActionResult> DateRange(string language = MarkdownBaseService.EnglishLanguage)
{
var allPosts = await blogViewService.GetAllPosts();
if (allPosts is null || !allPosts.Any())
{
return Json(new
{
minDate = DateTime.UtcNow.AddYears(-1).ToString("yyyy-MM-dd"),
maxDate = DateTime.UtcNow.ToString("yyyy-MM-dd")
});
}
var posts = allPosts;
if (!string.IsNullOrEmpty(language) && language != MarkdownBaseService.EnglishLanguage)
{
posts = allPosts.Where(p => p.Language.Equals(language, StringComparison.OrdinalIgnoreCase)).ToList();
}
if (!posts.Any()) posts = allPosts;
var minDate = posts.Min(p => p.PublishedDate.Date);
var maxDate = posts.Max(p => p.PublishedDate.Date);
return Json(new
{
minDate = minDate.ToString("yyyy-MM-dd"),
maxDate = maxDate.ToString("yyyy-MM-dd")
});
}
paramètre qui renvoie l'ensemble de jours dans un mois donné qui ont des messages (pour les faits saillants)LinkUrlA
public class BasePagingModel<T> : Interfaces.IPagingModel<T> where T : class
{
public int Page { get; set; }
public int TotalItems { get; set; } = 0;
public int PageSize { get; set; }
public ViewType ViewType { get; set; } = ViewType.TailwindAndDaisy;
public string LinkUrl { get; set; }
public List<T> Data { get; set; }
}
sequenceDiagram
participant U as User
participant FP as Flatpickr
participant JS as blog-index.js
participant HT as HTMX
participant C as BlogController
U->>FP: Selects 2025-11-01 → 2025-11-10
FP-->>JS: onChange([start,end])
JS->>JS: Update URLSearchParams (startDate, endDate, language, order)
JS->>HT: htmx.ajax('GET', /blog?...)
HT->>C: GET /blog with query
C-->>HT: Partial _BlogSummaryList
HT-->>U: Swap #content
JS->>FP: (after swap) ensure highlights + redraw
flowchart TD A[Open calendar / Month change] --> B[Fetch /blog/calendar-days] B -->|JSON dates: yyyy-mm-dd array| C[Set highlightDates] C --> D[flatpickr redraw] D --> E[onDayCreate adds .has-post]
Flux: faits saillants du calendrierBlogPostEntity:
public class BlogPostEntity
{
// ... existing fields ...
public bool IsPinned { get; set; }
public bool IsHidden { get; set; }
public DateTimeOffset? ScheduledPublishDate { get; set; }
}
Nouvelles fonctionnalités: Visibilité et contrôle du contenuBlogServiceFiltres post-visibilité
// Filter out hidden posts and posts scheduled for the future
var now = DateTimeOffset.UtcNow;
postQuery = postQuery.Where(x =>
!x.IsHidden &&
(x.ScheduledPublishDate == null || x.ScheduledPublishDate <= now));
// For page 1, prioritize pinned posts
var isFirstPage = page == null || page.Value == 1;
if (isFirstPage)
{
postQuery = postQuery.OrderByDescending(x => x.IsPinned)
.ThenByDescending(x => x.PublishedDate.DateTime);
}
La plus grande amélioration de cette mise à jour est une bonne gestion de la visibilité.
IsHidden = trueLesScheduledPublishDateIsPinned = true) n'apparaissent jamais dans les listesn'apparaissent qu'après leur
[HtmlTargetElement("clear-param")]
public class ClearParamTagHelper : TagHelper
{
[HtmlAttributeName("name")]
public string? Name { get; set; }
[HtmlAttributeName("all")]
public bool All { get; set; } = false;
[HtmlAttributeName("exclude")]
public string Exclude { get; set; } = "";
// ... styling and Alpine.js integration ...
}
Postes épinglés
<!-- Clear a specific parameter -->
<clear-param name="startDate">Clear Date</clear-param>
<!-- Clear all parameters except language -->
<clear-param all="true" exclude="language">Clear All Filters</clear-param>
) apparaissent toujours en premier à la page 1, parfait pour les annonces ou le contenuwindow.queryParamClearerEffacer l'aide de la balise Param
**Un nouvel helper ASP.NET Core permet de définir facilement les paramètres de la requête :**Utilisation dans les vues Razor:
composant pour gérer la manipulation d'URL et les swaps HTMX.
startDate/endDate.new URL(window.location.href)Je vais couvrir tous les détails de mise en œuvre, y compris le composant Alpine.js et les modèles d'aide de tag, dans un futur post sur la construction de composants HTMX/Alpine réutilisables.language/orderBugs que j'ai frappé (et les corrections)Gamme de dates perdues lors du changement de langue/commande
et ne changer que les parties qui ont changé; ou lire les dates de Flatpickr si présent.
<html class>Les temps forts du calendrier n'ont pas été rafraîchis après un swap HTMXfp.redraw().Cause : L'instance Flatpickr a vécu sur le vieux DOM et est morte lors de l'échange.
LinkUrlDe plus, il a repris après les swaps (voir ci-dessous).posts.LinkUrl = Url.Action("Index","Blog", new { startDate, endDate, language, orderBy, orderDir })Cause: les classes de style ont changé mais Flatpickr n'a pas redessiné les jours peints.changements et appel
(function(){
function initFromRoot(root){ /* …the big function shown above… */ }
if(window.htmx){
document.body.addEventListener('htmx:afterSwap', function(ev){
const tgt = ev.detail.target;
// re-init if the content swapped includes the filters/content area
if(tgt && (tgt.id === 'content' || tgt.querySelector?.('#filters'))){
initFromRoot(document);
}
});
}
// also run on first load
initFromRoot(document);
})();
donc le pager compose correctement les URLs.
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.