| -rw-r--r-- | Dockerfile | 2 | ||||
| -rw-r--r-- | internal/frontmatter/parser.go | 1 | ||||
| -rw-r--r-- | internal/lightgallery/block.go | 11 | ||||
| -rw-r--r-- | internal/lightgallery/html_renderer.go | 30 | ||||
| -rw-r--r-- | internal/lightgallery/parser.go | 35 | ||||
| -rw-r--r-- | main.go | 33 | ||||
| -rwxr-xr-x | saya-today-web | bin | 19752119 -> 0 bytes | |||
| -rw-r--r-- | static/input.css | 32 | ||||
| -rw-r--r-- | views/layouts/general-page.html | 4 | ||||
| -rw-r--r-- | views/pages/blog-catalogue.html | 38 | ||||
| -rw-r--r-- | views/pages/blog-page.html | 14 | ||||
| -rw-r--r-- | views/partials/catalogue-blog-cards.html | 14 |
12 files changed, 74 insertions, 140 deletions
@@ -16,6 +16,4 @@ COPY --from=build-stage /builddir/views /app/views COPY --from=build-stage /builddir/locale/*.yaml /app/locale/ COPY --from=build-stage /builddir/config/config.yaml /app/config.yaml -RUN apk add --no-cache tzdata - ENTRYPOINT [ "/app/sayana-demo", "-c", "/app/config.yaml" ] diff --git a/internal/frontmatter/parser.go b/internal/frontmatter/parser.go index 3c00a7f..1a521c0 100644 --- a/internal/frontmatter/parser.go +++ b/internal/frontmatter/parser.go @@ -16,7 +16,6 @@ type Metadata struct { Thumbnail string `yaml:"thumbnail"` Tags []string `yaml:"tags"` Geolocation string `yaml:"geolocation"` - Timezone string `yaml:"timezone"` } func ParseFrontmatter(content []byte) (metadata *Metadata, markdown []byte, err error) { diff --git a/internal/lightgallery/block.go b/internal/lightgallery/block.go index 8eb7c9e..dade2ea 100644 --- a/internal/lightgallery/block.go +++ b/internal/lightgallery/block.go @@ -1,21 +1,16 @@ package lightgallery -import ( - "time" - - "github.com/yuin/goldmark/ast" -) +import "github.com/yuin/goldmark/ast" // LightGalleryBlock represents a light gallery block in the AST type LightGalleryBlock struct { ast.BaseBlock - Images []LightGalleryImage - Location *time.Location + Images []LightGalleryImage } type LightGalleryImage struct { URL string - Caption []byte + Caption string } var KindLightGalleryBlock = ast.NewNodeKind("LightGalleryBlock") diff --git a/internal/lightgallery/html_renderer.go b/internal/lightgallery/html_renderer.go index 850cd95..e611f0b 100644 --- a/internal/lightgallery/html_renderer.go +++ b/internal/lightgallery/html_renderer.go @@ -1,13 +1,11 @@ package lightgallery import ( - "bytes" "fmt" "math/rand" "strings" "time" - "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" @@ -16,18 +14,11 @@ import ( type LightGalleryHTMLRenderer struct { html.Config - md goldmark.Markdown } func NewLightGalleryHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { r := &LightGalleryHTMLRenderer{ Config: html.NewConfig(), - md: goldmark.New( - goldmark.WithRendererOptions( - html.WithHardWraps(), - html.WithXHTML(), - ), - ), } for _, opt := range opts { opt.SetHTMLOption(&r.Config) @@ -62,17 +53,7 @@ func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source [ imageUrlWithoutExt := strings.Join(imageUrlSegments[:len(imageUrlSegments)-1], ".") imageNameParts := strings.Split(imageUrlWithoutExt, "-") - var captionBuf bytes.Buffer - captionHTML := img.Caption - if err := r.md.Convert(img.Caption, &captionBuf); err == nil { - captionHTML = captionBuf.Bytes() - captionHTML = bytes.TrimPrefix(captionHTML, []byte("<p>")) - captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>\n")) - captionHTML = bytes.TrimSuffix(captionHTML, []byte("</p>")) - } - dayDate, _ := time.Parse("20060102 150405", imageNameParts[len(imageNameParts)-2]+" "+imageNameParts[len(imageNameParts)-1]) - dayDate = dayDate.In(gallery.Location) dynamicElements = append(dynamicElements, fmt.Sprintf(`{ src: "https://f003.backblazeb2.com/file/sayana-photos/full/%s", @@ -89,7 +70,7 @@ func (r *LightGalleryHTMLRenderer) renderLightGallery(w util.BufWriter, source [ <p class="grow !text-[1vmax]/[0.9] text-left font-spectral text-main-dark">%s</p> <p class="!text-[1vmax]/[0.9] text-right font-spectral text-secondary">%s</p> </div>`+"`"+` - }`, img.URL, img.URL, util.EscapeHTML(captionHTML), imageUrlWithoutExt, imageUrlWithoutExt, captionHTML, dayDate.Format("2006-01-02 15:04:05 -07:00"))) + }`, img.URL, img.URL, img.Caption, imageUrlWithoutExt, imageUrlWithoutExt, img.Caption, dayDate.Format("2006-01-02 15:04:05 -07:00"))) } w.WriteString(fmt.Sprintf(` @@ -136,6 +117,15 @@ window.addEventListener("resize", () => { return ast.WalkContinue, nil } +func escapeHTML(s string) string { + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, "\"", """) + s = strings.ReplaceAll(s, "'", "'") + return s +} + func generateDivId(length int) string { const charset = "abcdefghijklmnopqrstuvwxyz" seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) // Seed with current time diff --git a/internal/lightgallery/parser.go b/internal/lightgallery/parser.go index daec797..0ce209c 100644 --- a/internal/lightgallery/parser.go +++ b/internal/lightgallery/parser.go @@ -2,9 +2,7 @@ package lightgallery import ( "bytes" - "log/slog" - "regexp" - "time" + "strings" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" @@ -24,52 +22,43 @@ func (p *LightGalleryParser) Trigger() []byte { func (p *LightGalleryParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { line, _ := reader.PeekLine() - if !bytes.HasPrefix(line, []byte("{Gallery:")) { + if !bytes.HasPrefix(line, []byte("{Gallery}")) { return nil, parser.NoChildren } - r := regexp.MustCompile(`^\{Gallery:([A-Za-z0-9\+\-/]+)\}$`) - trimmed := bytes.TrimSpace(line) - parts := r.FindSubmatch(trimmed) - if len(parts) < 2 { - slog.Warn("invalid gallery header format", slog.String("line", string(trimmed)), slog.Int("submatch_count", len(parts))) + if !bytes.Equal(trimmed, []byte("{Gallery}")) { return nil, parser.NoChildren } - loc, err := time.LoadLocation(string(parts[1])) - if err != nil { - slog.Warn("invalid location specified for a gallery", slog.String("error", err.Error()), slog.String("line", string(trimmed)), slog.String("location", string(parts[1]))) - return nil, parser.NoChildren - } - - return &LightGalleryBlock{Location: loc}, parser.NoChildren + return &LightGalleryBlock{}, parser.NoChildren } func (p *LightGalleryParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { line, segment := reader.PeekLine() if len(line) == 0 || segment.Len() == 0 { - return parser.Continue | parser.NoChildren + return parser.Close } trimmed := bytes.TrimSpace(line) - if bytes.Equal(trimmed, []byte("{Gallery}")) || bytes.Equal(trimmed, []byte("{/Gallery}")) { + if bytes.Equal(trimmed, []byte("{Gallery}")) { reader.AdvanceLine() return parser.Close } gallery := node.(*LightGalleryBlock) + lineStr := string(trimmed) - parts := bytes.SplitN(trimmed, []byte{'|'}, 2) - url := bytes.TrimSpace(parts[0]) + parts := strings.SplitN(lineStr, "|", 2) + url := strings.TrimSpace(parts[0]) + caption := "" - caption := make([]byte, 0) if len(parts) > 1 { - caption = bytes.TrimSpace(parts[1]) + caption = strings.TrimSpace(parts[1]) } gallery.Images = append(gallery.Images, LightGalleryImage{ - URL: string(url), + URL: url, Caption: caption, }) @@ -121,7 +121,7 @@ func main() { querySort := c.Query("sort") if querySort == "" { - querySort = "publicationDateDesc" + querySort = "titleAsc" } encodedQuery := c.Request().URI().QueryString() @@ -146,26 +146,19 @@ func main() { pages = []*b2.BlogPage{} } - tagsMap := make(map[string]int) + tagsSet := make(map[string]struct{}) for _, page := range pages { - slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/"+lang+"/blog")) + slog.Debug("enlist page for catalogue", slog.Any("page", page), slog.String("endpoint", "/")) for _, tag := range page.Metadata.Tags { - tagsMap[tag]++ + tagsSet[tag] = struct{}{} } } - type Tag struct { - Name string `json:"Name" yaml:"name"` - Count int `json:"Count" yaml:"count"` + tagsArray := make([]string, 0, len(tagsSet)) + for tag := range tagsSet { + tagsArray = append(tagsArray, tag) } - - tagsArray := make([]Tag, 0, len(tagsMap)) - for tag, count := range tagsMap { - tagsArray = append(tagsArray, Tag{tag, count}) - } - slices.SortFunc(tagsArray, func(a Tag, b Tag) int { - return strings.Compare(a.Name, b.Name) - }) + slices.Sort(tagsArray) content, err := tm.Render("blog-catalogue", fiber.Map{ "QuerySort": querySort, @@ -265,7 +258,7 @@ func main() { "Link": page.Link, "ArticleLink": "/" + lang + "/blog/" + page.FileName, "Title": page.Metadata.Title, - "PublishedTime": page.Metadata.PublishedTime.Format("2006-01-02 15:04:05 -07:00"), + "PublishedTime": page.Metadata.PublishedTime.Format("2006-01-02 15:04:05 -0700"), "ActionDate": page.Metadata.ActionDate, "ShortDescription": page.Metadata.ShortDescription, "Thumbnail": page.Metadata.Thumbnail, @@ -287,12 +280,12 @@ func main() { case "actionDateDesc": return strings.Compare(b["ActionDate"], a["ActionDate"]) case "publicationDateAsc": - publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"]) - publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"]) + publishedTimeA, _ := time.Parse(time.RFC3339, a["PublishedTime"]) + publishedTimeB, _ := time.Parse(time.RFC3339, b["PublishedTime"]) return publishedTimeA.Compare(publishedTimeB) case "publicationDateDesc": - publishedTimeA, _ := time.Parse("2006-01-02 15:04:05 -07:00", a["PublishedTime"]) - publishedTimeB, _ := time.Parse("2006-01-02 15:04:05 -07:00", b["PublishedTime"]) + publishedTimeA, _ := time.Parse(time.RFC3339, a["PublishedTime"]) + publishedTimeB, _ := time.Parse(time.RFC3339, b["PublishedTime"]) return publishedTimeB.Compare(publishedTimeA) } return 0 diff --git a/saya-today-web b/saya-today-web Binary files differdeleted file mode 100755 index b6522e8..0000000 --- a/saya-today-web +++ /dev/null diff --git a/static/input.css b/static/input.css index cfc7391..8776b25 100644 --- a/static/input.css +++ b/static/input.css @@ -88,15 +88,6 @@ --squares-and-triangles-background-height-coef: 15; } -a { - color: var(--main-medium-color) !important; - text-decoration: underline; -} - -a:hover { - color: var(--main-light-color) !important; -} - .bg-interlocked-hexagons { background-image: var(--interlocked-hexagons-background); background-repeat: var(--interlocked-hexagons-background-repeat); @@ -163,6 +154,10 @@ a:hover { background-size: calc(var(--squares-and-triangles-background-width-coef) * 1vw) calc(var(--squares-and-triangles-background-height-coef) * 1vw); } +body:has(.lg-container:not([class~="lg-inline"])) .desktop-sidebar-custom { + display: none !important; +} + .mobile-topbar-custom { width: 90vw; height: 8%; @@ -170,7 +165,7 @@ a:hover { margin-left: 5vw; } -body:has(.lg-container:not([class~="lg-inline"])) #sidebar { +body:has(.lg-container:not([class~="lg-inline"])) .mobile-topbar-custom { display: none !important; } @@ -341,11 +336,6 @@ body:has(.lg-container:not([class~="lg-inline"])) #sidebar { } } -body[data-theme~="night"] .leaflet-tile, -body[data-theme~="ram"] .leaflet-tile { - filter: invert(1) hue-rotate(190deg) contrast(1.1) brightness(1.2) !important; -} - @media (min-aspect-ratio: 0.8/1) { .ar-gt-0\.8\:mr-\[1\.2vmin\] { margin-right: 1.2vmin; @@ -426,10 +416,6 @@ body[data-theme~="ram"] .leaflet-tile { height: 10vh; } - .ar-lt-0\.8\:h-\[30vh\] { - height: 30vh; - } - .ar-lt-0\.8\:h-\[90vh\] { height: 90vh; } @@ -438,10 +424,6 @@ body[data-theme~="ram"] .leaflet-tile { width: 8vh; } - .ar-lt-0\.8\:w-\[80\%\] { - width: 80%; - } - .ar-lt-0\.8\:w-\[90vw\] { width: 90vw; } @@ -454,10 +436,6 @@ body[data-theme~="ram"] .leaflet-tile { max-height: 80%; } - .ar-lt-0\.8\:max-h-\[calc\(100\%-25vh\)\] { - max-height: calc(100% - 25vh); - } - .ar-lt-0\.8\:text-\[5vh\] { font-size: 5vh; } diff --git a/views/layouts/general-page.html b/views/layouts/general-page.html index eab7cd1..d5146f8 100644 --- a/views/layouts/general-page.html +++ b/views/layouts/general-page.html @@ -33,7 +33,7 @@ switchTheme(savedTheme); </script> - <div id="sidebar" class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> + <div class="bg-sidebar flex sticky flex-col ar-gt-0.8:items-center shadow-2xl z-20 w-[5vw] ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:h-[8vh] ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> <div class="logo-custom ar-lt-0.8:hidden text-sidebar font-patua font-extrabold text-center relative z-20"> saya.today </div> @@ -48,7 +48,7 @@ <div class="bg-background-dark flex z-10 ar-lt-0.8:w-[90vw] ar-lt-0.4:w-[100vw] h-screen ar-lt-0.8:ml-[5vw] ar-lt-0.4:ml-0"> <div class="pl-[0.5vw] bg-background-dark border-r-[0.4vmin] border-dashed border-main-dark"></div> - <div class="ar-gt-0.8:min-w-fit ar-gt-0.8:max-w-[100vh] flex flex-col bg-background-dark relative pt-[1.6vmin] ml-[1.6vmin] pr-[1.6vmin]"> + <div class="ar-gt-0.8:min-w-fit ar-gt-0.8:max-w-[100vh] flex flex-col bg-background-dark relative mt-[1.6vmin] ml-[1.6vmin] pr-[1.6vmin]"> <div class="flex flex-row mb-[0.4vmin]"> <i onclick="location.pathname += location.pathname.endsWith('/') ? '../' : '/../';" class="fas fa-circle-left hover:bg-main-dark hover:bg-opacity-10 cursor-pointer transition-colors duration-300 rounded-md text-[2vmax] mt-auto mb-auto px-[0.8vmin]"></i> <p class="text-[2vmax] font-spectral italic font-extrabold select-none mt-auto pl-[0.8vmin] pr-[1.2vmin]">//</p> diff --git a/views/pages/blog-catalogue.html b/views/pages/blog-catalogue.html index 6c0f629..6fe1c82 100644 --- a/views/pages/blog-catalogue.html +++ b/views/pages/blog-catalogue.html @@ -1,7 +1,7 @@ {{ define "body" }} -<div class="flex flex-row ar-lt-0.8:flex-col max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)] flex-1 text-[1vmax]"> +<div class="flex flex-row ar-lt-0.8:flex-col flex-1 text-[1vmax]"> <form hx-get="/api/v1/blog-search" hx-vals='{"lang": "{{ .Lang }}"}' hx-target=".blog-cards" hx-swap="innerHTML" - class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] flex shrink-0 flex-col overflow-y-auto ar-gt-0.8:mr-[1.2vmin]"> + class="tags-list ar-gt-0.8:min-w-[10vh] ar-gt-0.8:max-w-[20vh] ar-gt-0.8:w-fit ar-lt-0.8:w-[100%] flex shrink-0 flex-col ar-gt-0.8:mr-[1.2vmin]"> <fieldset> <legend class="font-bold w-[100%] text-center">{{ .L.BlogSearch.TagsHeader }}</legend> <div class="flex flex-col ar-lt-0.8:grid grid-cols-5 grid-flow-row-dense gap-[5%]"> @@ -10,7 +10,7 @@ </div> {{- range .Tags }} <div> - <label><input type="checkbox" id="tag{{ .Name }}Checkbox" name="tags[]" value="{{ .Name }}" onclick="contextAll();" {{ if contains $.QueryTags .Name }}checked{{ end }}> {{ .Name }} <span class="text-secondary italic">{{ .Count }}</span></label> + <label><input type="checkbox" id="tag{{ . }}Checkbox" name="tags[]" value="{{ . }}" onclick="contextAll();" {{ if contains $.QueryTags . }}checked{{ end }}> {{ . }}</label> </div> {{- end }} </div> @@ -60,6 +60,20 @@ <script src="https://f003.backblazeb2.com/file/sayana-static/libs/htmx/2.0.6/htmx.min.js"></script> <script> + const tagsContainers = document.querySelectorAll('form.tags-list'); + tagsContainers.forEach(tagsContainer => { + const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); + anyChecked = false; + checkboxes.forEach(checkbox => { + anyChecked = checkbox.checked ? true : anyChecked; + }); + if (!anyChecked) { + selectAll(); + } + }); +</script> + +<script> function selectAll() { const tagsContainers = document.querySelectorAll('form.tags-list'); tagsContainers.forEach(tagsContainer => { @@ -91,7 +105,7 @@ checkboxes.forEach(checkbox => { checkbox.checked = true; }); - const sortRadio = tagsContainer.querySelector('#sortPublicationDateDesc'); + const sortRadio = tagsContainer.querySelector('#sortTitleAsc'); sortRadio.checked = true; }); } @@ -123,20 +137,4 @@ document.addEventListener('DOMContentLoaded', contextAll); </script> -<script> - const tagsContainers = document.querySelectorAll('form.tags-list'); - tagsContainers.forEach(tagsContainer => { - const checkboxes = tagsContainer.querySelectorAll('input[type="checkbox"]'); - anyChecked = false; - checkboxes.forEach(checkbox => { - anyChecked = checkbox.checked ? true : anyChecked; - }); - if (!anyChecked) { - const selectAllCheckbox = tagsContainer.querySelector('#tagsAllCheckbox'); - selectAllCheckbox.checked = true; - selectAll(); - } - }); -</script> - {{ end }} diff --git a/views/pages/blog-page.html b/views/pages/blog-page.html index 330a1ac..5f001d6 100644 --- a/views/pages/blog-page.html +++ b/views/pages/blog-page.html @@ -15,11 +15,11 @@ {{ end }} {{ define "body" }} -<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] max-h-[calc(100%-7vmax)] ar-lt-0.8:max-h-[calc(100%-25vh)]"> +<div class="bg-background-light flex flex-col grow overflow-y-auto inset-shadow p-[2.4vmin] ar-lt-0.8:max-h-[80%]"> {{ .ParsedMarkdown }} {{- if .MapLocationX }} <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> - <div id="location-map" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] ar-lt-0.8:w-[80%] h-[40vh] ar-lt-0.8:h-[30vh]"></div> + <div id="location-map" class="relative p-[0.8vmin] flex-none ml-auto mr-auto w-[60%] h-[40vh]"></div> <hr class="border-t-4 border-dotted border-main-dark mt-[0.8vmin] mb-[0.8vmin] w-[80%] ml-auto mr-auto"> {{- end }} </div> @@ -34,10 +34,10 @@ <script> function initLocationMap(id, x, y, relativeErrorMeters = 0) { - var map = L.map(id).setView([x, y], 8); + var map = L.map(id).setView([x, y], 13); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 18, + maxZoom: 19, attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' }).addTo(map); @@ -50,11 +50,7 @@ }).addTo(map); } - var marker = L.marker([x, y], { - title: '{{ .Title }}', - }).addTo(map) - .bindPopup('<span><b>{{ .Title }}</b><br><a href="https://www.openstreetmap.org/#map=13/{{ .MapLocationX }}/{{ .MapLocationY }}">{{ .MapLocationX }}, {{ .MapLocationY }}</a></span>') - .openPopup(); + var marker = L.marker([x, y]).addTo(map); } document.addEventListener('DOMContentLoaded', () => {initLocationMap( diff --git a/views/partials/catalogue-blog-cards.html b/views/partials/catalogue-blog-cards.html index a9e4010..36a6184 100644 --- a/views/partials/catalogue-blog-cards.html +++ b/views/partials/catalogue-blog-cards.html @@ -1,15 +1,13 @@ {{- range .BlogPages }} <hr class="first-of-type:hidden border-t-[0.3vh] my-[0.8vmin] border-dotted border-main-dark w-[80%] mx-auto"> <div class="m-[0.4vmin] flex flex-row justify-center justify-items-center"> - <img onclick="location.href='{{ .ArticleLink }}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[10vmin] h-[10vmin] aspect-square my-[0.4vmin] mr-[0.8vmin]" src="https://f003.backblazeb2.com/file/sayana-photos/thumbnails/{{ .Thumbnail }}.webp"> + <img onclick="location.href='{{ .ArticleLink }}}';" class="cursor-pointer object-cover rounded-[10%] select-none w-[10vmin] h-[10vmin] aspect-square my-[0.4vmin] mr-[0.8vmin]" src="https://f003.backblazeb2.com/file/sayana-photos/thumbnails/{{ .Thumbnail }}.webp"> <div class="grow my-[0.4vmin] mx-[0.8vmin] flex flex-col justify-center"> - <div class="flex flex-row gap-[5%] max-w-[100%]"> - <a href="{{ .ArticleLink }}" class="flex-9/12 grow-0 block text-[1vmax] cursor-pointer"> - <span class="font-extrabold">{{ .Title }}</span> - <span class="font-extrabold select-none text-secondary">//</span> - <span class="italic text-secondary">{{ .ActionDate }}</span> - </a> - <p class="flex-3/12 grow-0 text-[1vmax] italic text-secondary text-right">{{ .PublishedTime }}</p> + <div class="flex flex-row"> + <a href="{{ .ArticleLink }}" class="block text-[1vmax] font-extrabold">{{ .Title }}</a> + <a href="{{ .ArticleLink }}" class="block text-[1vmax] font-extrabold pl-[0.4vmin] pr-[0.4vmin] select-none cursor-pointer text-secondary">//</a> + <a href="{{ .ArticleLink }}" class="block text-[1vmax] italic text-secondary">{{ .ActionDate }}</a> + <p class="text-[1vmax] italic text-secondary text-right grow">{{ .PublishedTime }}</p> </div> <p class="text-[1vmax] text-main-dark">{{ .ShortDescription }}</p> <p class="text-[1vmax] text-secondary">{{ .L.TagsLabel }} {{ .Tags }}</p> |