sitemap.nx source
↩ module page · 193 lines · 6892 B
1// sitemap.nx -- XML Sitemap 0.9 builder.
2//
3// Specification: sitemaps.org (Google/Yahoo/MSN 2005 joint draft).
4// Every search crawler uses it to discover pages it might not
5// reach via link-following. Referenced from robots.txt and served
6// at /sitemap.xml or /sitemap-index.xml.
7//
8// Skeleton:
9// <?xml version=\"1.0\" encoding=\"utf-8\"?>
10// <urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">
11// <url>
12// <loc>https://example.com/foo</loc>
13// <lastmod>2026-04-22</lastmod>
14// <changefreq>weekly</changefreq>
15// <priority>0.8</priority>
16// </url>
17// ...
18// </urlset>
19//
20// Soft caps (per spec): 50,000 URLs per sitemap, 50 MiB uncompressed.
21// For larger sites use a sitemap index (not implemented here yet;
22// a future sitemap_index.nx will compose this).
23//
24// Invariants:
25// S1 loc is required; lastmod/changefreq/priority optional.
26// S2 lastmod is date-only format YYYY-MM-DD (from unix_sec
27// via civil_date.nx). Sitemaps accept ISO 8601 but
28// date-only is canonical and smaller.
29// S3 changefreq values: always, hourly, daily, weekly, monthly,
30// yearly, never. We surface them as constants; caller picks.
31// S4 Stream builder: sitemap_begin -> sitemap_url* -> sitemap_end.
32
33import "syscalls.nx"
34import "html_escape.nx"
35import "civil_date.nx"
36
37const SM_ERR_SHORT: i64 = -1
38
39const SM_FREQ_ALWAYS: i64 = 1
40const SM_FREQ_HOURLY: i64 = 2
41const SM_FREQ_DAILY: i64 = 3
42const SM_FREQ_WEEKLY: i64 = 4
43const SM_FREQ_MONTHLY: i64 = 5
44const SM_FREQ_YEARLY: i64 = 6
45const SM_FREQ_NEVER: i64 = 7
46const SM_FREQ_NONE: i64 = 0
47
48func sm_put(out: *u8, cap: i64, off: i64, src: *u8, n: i64) -> i64 {
49 if off + n > cap { return SM_ERR_SHORT }
50 var i: i64 = 0
51 while i < n {
52 out[off + i] = src[i]
53 i = i + 1
54 }
55 return off + n
56}
57
58func sm_put_escaped(out: *u8, cap: i64, off: i64,
59 src: *u8, n: i64) -> i64 {
60 let w: i64 = html_escape(out + off, cap - off, src, n)
61 if w < 0 { return SM_ERR_SHORT }
62 return off + w
63}
64
65// Write 4-digit year, 2-digit month, 2-digit day: YYYY-MM-DD.
66func sm_put_date(out: *u8, cap: i64, off: i64, unix_sec: i64) -> i64 {
67 if off + 10 > cap { return SM_ERR_SHORT }
68 let cd_raw: *u8 = sys_mmap(128)
69 let cd: *CivilDate = cd_raw as *CivilDate
70 civil_from_unix(unix_sec, cd)
71 out[off] = 0x30 + ((cd.year / 1000) % 10)
72 out[off + 1] = 0x30 + ((cd.year / 100) % 10)
73 out[off + 2] = 0x30 + ((cd.year / 10) % 10)
74 out[off + 3] = 0x30 + (cd.year % 10)
75 out[off + 4] = 0x2D
76 out[off + 5] = 0x30 + ((cd.month / 10) % 10)
77 out[off + 6] = 0x30 + (cd.month % 10)
78 out[off + 7] = 0x2D
79 out[off + 8] = 0x30 + ((cd.day / 10) % 10)
80 out[off + 9] = 0x30 + (cd.day % 10)
81 return off + 10
82}
83
84// Write a changefreq value based on SM_FREQ_*. Returns new
85// offset or unchanged if freq == SM_FREQ_NONE.
86func sm_put_freq(out: *u8, cap: i64, off: i64, freq: i64) -> i64 {
87 if freq == SM_FREQ_NONE { return off }
88 var cur: i64 = off
89 cur = sm_put(out, cap, cur, "<changefreq>", 12)
90 if cur < 0 { return cur }
91 if freq == SM_FREQ_ALWAYS { cur = sm_put(out, cap, cur, "always", 6) }
92 if freq == SM_FREQ_HOURLY { cur = sm_put(out, cap, cur, "hourly", 6) }
93 if freq == SM_FREQ_DAILY { cur = sm_put(out, cap, cur, "daily", 5) }
94 if freq == SM_FREQ_WEEKLY { cur = sm_put(out, cap, cur, "weekly", 6) }
95 if freq == SM_FREQ_MONTHLY { cur = sm_put(out, cap, cur, "monthly", 7) }
96 if freq == SM_FREQ_YEARLY { cur = sm_put(out, cap, cur, "yearly", 6) }
97 if freq == SM_FREQ_NEVER { cur = sm_put(out, cap, cur, "never", 5) }
98 if cur < 0 { return cur }
99 cur = sm_put(out, cap, cur, "</changefreq>\n", 14)
100 return cur
101}
102
103// Emit opening + urlset.
104func sitemap_begin(out: *u8, cap: i64, off: i64) -> i64 {
105 var cur: i64 = off
106 cur = sm_put(out, cap, cur,
107 "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n", 38)
108 if cur < 0 { return cur }
109 cur = sm_put(out, cap, cur,
110 "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n", 62)
111 return cur
112}
113
114// Emit one <url>. Pass -1 for lastmod_unix to omit it.
115// Pass SM_FREQ_NONE for freq to omit changefreq.
116// Pass -1 for priority_tenths to omit. Priority is an integer
117// 0..10 (tenths of a unit); written as 0.N.
118func sitemap_url(out: *u8, cap: i64, off: i64,
119 loc: *u8, loc_len: i64,
120 lastmod_unix: i64,
121 freq: i64,
122 priority_tenths: i64) -> i64 {
123 var cur: i64 = off
124 cur = sm_put(out, cap, cur, "<url>\n<loc>", 11)
125 if cur < 0 { return cur }
126 cur = sm_put_escaped(out, cap, cur, loc, loc_len)
127 if cur < 0 { return cur }
128 cur = sm_put(out, cap, cur, "</loc>\n", 7)
129 if cur < 0 { return cur }
130
131 if lastmod_unix >= 0 {
132 cur = sm_put(out, cap, cur, "<lastmod>", 9)
133 if cur < 0 { return cur }
134 cur = sm_put_date(out, cap, cur, lastmod_unix)
135 if cur < 0 { return cur }
136 cur = sm_put(out, cap, cur, "</lastmod>\n", 11)
137 if cur < 0 { return cur }
138 }
139
140 cur = sm_put_freq(out, cap, cur, freq)
141 if cur < 0 { return cur }
142
143 if priority_tenths >= 0 {
144 if priority_tenths <= 10 {
145 cur = sm_put(out, cap, cur, "<priority>", 10)
146 if cur < 0 { return cur }
147 if priority_tenths == 10 {
148 cur = sm_put(out, cap, cur, "1.0", 3)
149 } else {
150 if cur + 3 > cap { return SM_ERR_SHORT }
151 out[cur] = 0x30
152 out[cur + 1] = 0x2E
153 out[cur + 2] = 0x30 + priority_tenths
154 cur = cur + 3
155 }
156 if cur < 0 { return cur }
157 cur = sm_put(out, cap, cur, "</priority>\n", 12)
158 if cur < 0 { return cur }
159 }
160 }
161
162 cur = sm_put(out, cap, cur, "</url>\n", 7)
163 return cur
164}
165
166func sitemap_end(out: *u8, cap: i64, off: i64) -> i64 {
167 return sm_put(out, cap, off, "</urlset>\n", 10)
168}
169
170// Compile-only smoke.
171func main() -> i64 {
172 let out: *u8 = sys_mmap(4096)
173 var off: i64 = 0
174 off = sitemap_begin(out, 4096, off)
175 if off < 0 { return 1 }
176
177 off = sitemap_url(out, 4096, off,
178 "https://nishifamily.com/", 24,
179 1777000000, SM_FREQ_WEEKLY, 10)
180 if off < 0 { return 2 }
181
182 off = sitemap_url(out, 4096, off,
183 "https://nishifamily.com/blog/1", 30,
184 1777000000, SM_FREQ_MONTHLY, 8)
185 if off < 0 { return 3 }
186
187 off = sitemap_end(out, 4096, off)
188 if off < 0 { return 4 }
189
190 if out[0] != 0x3C { return 5 }
191 if out[1] != 0x3F { return 6 }
192 return 0
193}