chore: updated date format for posts 8f40a0a8
Steve Simkins · 2026-05-21 07:49 5 file(s) · +86 −18
apps/posts/db.go +52 −11
24 24
    tags            TEXT,
25 25
    content         TEXT NOT NULL,
26 26
    status          TEXT NOT NULL DEFAULT 'draft',
27 -
    created_at      TEXT NOT NULL DEFAULT (datetime('now')),
28 -
    updated_at      TEXT NOT NULL DEFAULT (datetime('now'))
27 +
    created_at      TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
28 +
    updated_at      TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
29 29
);
30 30
31 31
CREATE TABLE IF NOT EXISTS pages (
36 36
    content         TEXT NOT NULL,
37 37
    is_published    INTEGER NOT NULL DEFAULT 0,
38 38
    nav_order       INTEGER NOT NULL DEFAULT 0,
39 -
    created_at      TEXT NOT NULL DEFAULT (datetime('now')),
40 -
    updated_at      TEXT NOT NULL DEFAULT (datetime('now'))
39 +
    created_at      TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
40 +
    updated_at      TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
41 41
);
42 42
43 43
CREATE TABLE IF NOT EXISTS settings (
52 52
    original_name   TEXT NOT NULL,
53 53
    content_type    TEXT NOT NULL DEFAULT 'application/octet-stream',
54 54
    size            INTEGER NOT NULL,
55 -
    created_at      TEXT NOT NULL DEFAULT (datetime('now')),
55 +
    created_at      TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
56 56
    storage_backend TEXT NOT NULL DEFAULT 'local'
57 57
);
58 58
`
148 148
	if err != nil {
149 149
		return nil, err
150 150
	}
151 +
	in.PublishedDate = normalizePubDatePtr(in.PublishedDate)
151 152
	res, err := db.Exec(
152 153
		`INSERT INTO posts (short_id, title, slug, content, status, alias, canonical_url, published_date, meta_description, meta_image, lang, tags)
153 154
		 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
209 210
}
210 211
211 212
func updatePost(db *sql.DB, shortID string, in PostInput) (*Post, error) {
213 +
	in.PublishedDate = normalizePubDatePtr(in.PublishedDate)
212 214
	res, err := db.Exec(
213 215
		`UPDATE posts SET title = ?, slug = ?, content = ?, status = ?, alias = ?, canonical_url = ?,
214 -
		 published_date = CASE WHEN ? = 'published' THEN COALESCE(?, published_date, datetime('now')) ELSE ? END,
216 +
		 published_date = CASE WHEN ? = 'published' THEN COALESCE(?, published_date, strftime('%Y-%m-%dT%H:%M:%SZ','now')) ELSE ? END,
215 217
		 meta_description = ?, meta_image = ?, lang = ?, tags = ?,
216 -
		 updated_at = datetime('now') WHERE short_id = ?`,
218 +
		 updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE short_id = ?`,
217 219
		nullable(in.Title), in.Slug, in.Content, in.Status, nullable(in.Alias), nullable(in.CanonicalURL),
218 220
		in.Status, nullable(in.PublishedDate), nullable(in.PublishedDate),
219 221
		nullable(in.MetaDescription), nullable(in.MetaImage), in.Lang, nullable(in.Tags), shortID,
251 253
	}
252 254
	if newStatus == "published" {
253 255
		_, err = db.Exec(
254 -
			`UPDATE posts SET status = ?, published_date = COALESCE(published_date, datetime('now')), updated_at = datetime('now') WHERE short_id = ?`,
256 +
			`UPDATE posts SET status = ?, published_date = COALESCE(published_date, strftime('%Y-%m-%dT%H:%M:%SZ','now')), updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE short_id = ?`,
255 257
			newStatus, shortID)
256 258
	} else {
257 -
		_, err = db.Exec(`UPDATE posts SET status = ?, updated_at = datetime('now') WHERE short_id = ?`,
259 +
		_, err = db.Exec(`UPDATE posts SET status = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE short_id = ?`,
258 260
			newStatus, shortID)
259 261
	}
260 262
	return newStatus, err
338 340
		pub = 1
339 341
	}
340 342
	res, err := db.Exec(
341 -
		`UPDATE pages SET title = ?, slug = ?, content = ?, is_published = ?, nav_order = ?, updated_at = datetime('now') WHERE short_id = ?`,
343 +
		`UPDATE pages SET title = ?, slug = ?, content = ?, is_published = ?, nav_order = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now') WHERE short_id = ?`,
342 344
		title, slug, content, pub, navOrder, shortID)
343 345
	if err != nil {
344 346
		return nil, err
437 439
}
438 440
439 441
func nowDatetime() string {
440 -
	return time.Now().UTC().Format("2006-01-02 15:04:05")
442 +
	return time.Now().UTC().Format(time.RFC3339)
443 +
}
444 +
445 +
// migrateTimestamps rewrites legacy "YYYY-MM-DD HH:MM:SS" and date-only values
446 +
// to RFC3339 UTC. Idempotent: skips values already in RFC3339 form.
447 +
func migrateTimestamps(db *sql.DB) error {
448 +
	stmts := []string{
449 +
		`UPDATE posts SET published_date = REPLACE(published_date, ' ', 'T') || 'Z'
450 +
		 WHERE published_date IS NOT NULL AND length(published_date) = 19
451 +
		   AND published_date LIKE '____-__-__ __:__:__'`,
452 +
		`UPDATE posts SET published_date = published_date || 'T00:00:00Z'
453 +
		 WHERE published_date IS NOT NULL AND length(published_date) = 10
454 +
		   AND published_date LIKE '____-__-__'`,
455 +
		`UPDATE posts SET created_at = REPLACE(created_at, ' ', 'T') || 'Z'
456 +
		 WHERE length(created_at) = 19 AND created_at LIKE '____-__-__ __:__:__'`,
457 +
		`UPDATE posts SET updated_at = REPLACE(updated_at, ' ', 'T') || 'Z'
458 +
		 WHERE length(updated_at) = 19 AND updated_at LIKE '____-__-__ __:__:__'`,
459 +
		`UPDATE pages SET created_at = REPLACE(created_at, ' ', 'T') || 'Z'
460 +
		 WHERE length(created_at) = 19 AND created_at LIKE '____-__-__ __:__:__'`,
461 +
		`UPDATE pages SET updated_at = REPLACE(updated_at, ' ', 'T') || 'Z'
462 +
		 WHERE length(updated_at) = 19 AND updated_at LIKE '____-__-__ __:__:__'`,
463 +
		`UPDATE files SET created_at = REPLACE(created_at, ' ', 'T') || 'Z'
464 +
		 WHERE length(created_at) = 19 AND created_at LIKE '____-__-__ __:__:__'`,
465 +
	}
466 +
	for _, s := range stmts {
467 +
		if _, err := db.Exec(s); err != nil {
468 +
			return err
469 +
		}
470 +
	}
471 +
	return nil
472 +
}
473 +
474 +
func normalizePubDatePtr(p *string) *string {
475 +
	if p == nil {
476 +
		return nil
477 +
	}
478 +
	if v, ok := parsePubDate(*p); ok {
479 +
		return &v
480 +
	}
481 +
	return p
441 482
}
442 483
443 484
func _useStrings() {
apps/posts/main.go +3 −0
23 23
		log.Fatal(err)
24 24
	}
25 25
	defer db.Close()
26 +
	if err := migrateTimestamps(db); err != nil {
27 +
		log.Fatalf("migrate timestamps: %v", err)
28 +
	}
26 29
	seedDefaultSettings(db)
27 30
28 31
	uploadsDir := config.Getenv("UPLOADS_DIR", "uploads")
apps/posts/templates/admin_import.html +1 −1
20 20
title: My Post
21 21
slug: my-post
22 22
status: published
23 -
published_date: 2025-01-15 10:00:00
23 +
published_date: 2025-01-15T10:00:00Z
24 24
tags: rust, sqlite
25 25
description: A short summary
26 26
lang: en
apps/posts/templates/admin_post_form.html +2 −2
19 19
        <div class="fields-list">
20 20
          <span>title: My Post Title</span>
21 21
          <span>slug: my-post-title</span>
22 -
          <span>published_date: 2025-01-15 14:30:00</span>
22 +
          <span>published_date: 2025-01-15T14:30:00Z</span>
23 23
          <span>lang: en</span>
24 24
          <span>tags: rust, web, tutorial</span>
25 25
          <span>alias: /old/path</span>
47 47
        <div class="fields-list">
48 48
          <span>title: My Post Title</span>
49 49
          <span>slug: my-post-title</span>
50 -
          <span>published_date: 2025-01-15 14:30:00</span>
50 +
          <span>published_date: 2025-01-15T14:30:00Z</span>
51 51
          <span>lang: en</span>
52 52
          <span>tags: rust, web, tutorial</span>
53 53
          <span>alias: /old/path</span>
apps/posts/util.go +28 −4
167 167
	return out
168 168
}
169 169
170 -
func toRFC2822(sqliteTS string) string {
171 -
	if t, err := time.Parse("2006-01-02 15:04:05", sqliteTS); err == nil {
172 -
		return t.UTC().Format(time.RFC1123Z)
170 +
var pubDateLayouts = []string{
171 +
	time.RFC3339,
172 +
	"2006-01-02T15:04:05",
173 +
	"2006-01-02 15:04:05",
174 +
	"2006-01-02",
175 +
}
176 +
177 +
// parsePubDate accepts RFC3339, naive datetime, or date-only input and returns
178 +
// the value normalized to RFC3339 UTC. Returns ok=false if no layout matches.
179 +
func parsePubDate(s string) (string, bool) {
180 +
	s = strings.TrimSpace(s)
181 +
	if s == "" {
182 +
		return "", false
173 183
	}
174 -
	return sqliteTS
184 +
	for _, l := range pubDateLayouts {
185 +
		if t, err := time.Parse(l, s); err == nil {
186 +
			return t.UTC().Format(time.RFC3339), true
187 +
		}
188 +
	}
189 +
	return "", false
190 +
}
191 +
192 +
func toRFC2822(ts string) string {
193 +
	for _, l := range pubDateLayouts {
194 +
		if t, err := time.Parse(l, ts); err == nil {
195 +
			return t.UTC().Format(time.RFC1123Z)
196 +
		}
197 +
	}
198 +
	return ts
175 199
}
176 200
177 201
func xmlEscape(s string) string {