-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.py
More file actions
2482 lines (2236 loc) · 164 KB
/
Copy pathbuild.py
File metadata and controls
2482 lines (2236 loc) · 164 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
build.py — Static site generator for ibaneasy.com
Reads iban_countries.json and generates all SEO pages under src/
Also generates sitemap.xml and robots.txt
Usage: python build.py
Output: src/ directory with all generated pages + sitemap.xml + robots.txt
"""
import json
import os
import re
import random
from datetime import date
ROOT = os.path.dirname(os.path.abspath(__file__))
SRC = ROOT # Output to project root so Cloudflare Pages serves at correct URLs
DATA = os.path.join(ROOT, 'iban_countries.json')
SITE = 'https://ibaneasy.com'
TODAY = date.today().isoformat()
if not os.path.exists(DATA):
raise SystemExit(f'ERROR: {DATA} not found — run from project root')
with open(DATA, 'r', encoding='utf-8') as f:
countries = json.load(f)
# ── Python MOD-97 ────────────────────────────────────────────────
def mod97(num_str):
"""Python can handle huge integers natively, no chunking needed."""
return int(num_str) % 97
def letter_to_num(c):
return str(ord(c) - 55) # A=10, B=11, ..., Z=35
def to_numeric(s):
return ''.join(letter_to_num(c) if c.isalpha() else c for c in s)
def compute_check(country_code, bban):
rearranged = bban + country_code + '00'
numeric = to_numeric(rearranged)
remainder = mod97(numeric)
check = 98 - remainder
return str(check).zfill(2)
def generate_bban(bban_format):
parts = re.findall(r'(\d+)!([nac])', bban_format)
bban = ''
for count_str, t in parts:
count = int(count_str)
if t == 'n':
bban += ''.join(str(random.randint(0, 9)) for _ in range(count))
elif t == 'a':
bban += ''.join(chr(random.randint(65, 90)) for _ in range(count))
else: # c — alphanumeric
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
bban += ''.join(random.choice(chars) for _ in range(count))
return bban
def generate_iban(country):
bban = generate_bban(country['bbanFormat'])
check = compute_check(country['code'], bban)
return country['code'] + check + bban
def format_iban(iban):
"""Insert spaces every 4 characters."""
return ' '.join(iban[i:i+4] for i in range(0, len(iban), 4))
# ── Shared HTML Blocks ───────────────────────────────────────────
HEAD = '''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{title}</title>
<meta name="description" content="{desc}">
<link rel="canonical" href="{canon}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="IBAN Easy">
<meta property="og:title" content="{og_title}">
<meta property="og:description" content="{og_desc}">
<meta property="og:url" content="{canon}">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<script src="/js/theme.js"></script>
<link rel="stylesheet" href="/style.css">
<link rel="alternate" hreflang="en" href="https://ibaneasy.com/">
<link rel="alternate" hreflang="de" href="https://ibaneasy.com/de/">
<link rel="alternate" hreflang="es" href="https://ibaneasy.com/es/">
<link rel="alternate" hreflang="fr" href="https://ibaneasy.com/fr/">
<link rel="alternate" hreflang="zh" href="https://ibaneasy.com/zh/">
<link rel="alternate" hreflang="x-default" href="https://ibaneasy.com/">
{extra}
</head>
<body>
<div class="app-container">
<nav class="main-nav">
<div class="nav-inner">
<a class="nav-logo" href="/"><span class="lg-accent">IBAN</span><span class="lg-dim"> Easy</span></a>
<div class="nav-links">
<a href="/">Home</a>
<a href="/what-is-iban/">What is IBAN</a>
<a href="/countries/">Countries</a>
<a href="/validate/">Validator</a>
<a href="/sepa-countries/">SEPA</a>
<a href="/iban-check-digit/">Check Digits</a>
<a href="/swift-codes/">SWIFT Codes</a>
<a href="/iban-calculator/">Calculator</a>
<a href="/learn/">Learn</a>
<div class="lang-switch">
<button class="lang-btn" type="button" aria-expanded="false" aria-controls="lang-menu" onclick="toggleLangMenu()">
<span id="lang-current">🌐</span>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>
</button>
<div class="lang-menu" id="lang-menu">
<a href="/" data-lang="en">🇬🇧 English</a>
<a href="/de/" data-lang="de">🇩🇪 Deutsch</a>
<a href="/es/" data-lang="es">🇪🇸 Español</a>
<a href="/fr/" data-lang="fr">🇫🇷 Français</a>
<a href="/zh/" data-lang="zh">🇨🇳 中文</a>
</div>
</div>
<button class="theme-toggle" onclick="toggleTheme()" aria-label="Toggle dark/light theme" title="Toggle theme">
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg>
</button>
</div>
</div>
</nav>
'''
FOOT = '''</main>
<footer class="site-footer">
<div class="footer-inner">
<div>
<div class="footer-brand"><span class="fb-accent">IBAN</span><span class="fb-dim"> Easy</span></div>
<p class="footer-note">Free online IBAN tools. All generation and validation runs client-side. No data is collected or stored. Generated IBANs are for testing only.</p>
</div>
<div class="footer-links">
<a href="/">IBAN Generator</a>
<a href="/what-is-iban/">What is an IBAN?</a>
<a href="/countries/">All Countries</a>
<a href="/validate/">Validator</a>
<a href="/sepa-countries/">SEPA Countries</a>
<a href="/iban-check-digit/">Check Digits</a>
<a href="/swift-codes/">SWIFT/BIC Codes</a>
<a href="/iban-calculator/">IBAN Calculator</a>
<a href="/learn/">Learn</a>
<a href="/contact/">Contact</a>
<a href="/privacy/">Privacy</a>
<a href="/terms/">Terms</a>
<a href="/sitemap/">Sitemap</a>
</div>
</div>
</footer>
</div><!-- /app-container -->
</body>
</html>'''
# ── Schema Helpers ────────────────────────────────────────────────
def schema_script(data):
"""Wrap a dict/list as a JSON-LD script tag."""
return '<script type="application/ld+json">' + json.dumps(data, ensure_ascii=False) + '</script>\n'
def breadcrumb_schema(crumbs):
"""Build a BreadcrumbList schema from a list of (name, url) tuples."""
items = []
for i, (name, url) in enumerate(crumbs, start=1):
item = {'@type': 'ListItem', 'position': i, 'name': name}
if url:
item['item'] = url
items.append(item)
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
'itemListElement': items,
}
def itemlist_schema(items):
"""Build an ItemList schema from a list of (name, url) tuples."""
return {
'@context': 'https://schema.org',
'@type': 'ItemList',
'itemListElement': [
{'@type': 'ListItem', 'position': i, 'name': name, 'url': url}
for i, (name, url) in enumerate(items, start=1)
],
}
# ── Country Banking System Descriptions ───────────────────────────
# Used to add unique descriptive content to each country page
BANKING_NOTES = {
'DE': 'Germany has one of the largest banking sectors in Europe, with over 1,600 banks including Deutsche Bank, Commerzbank, and hundreds of cooperative and savings banks (Sparkassen, Volksbanken). All German IBANs start with DE followed by 20 characters including the 8-digit BLZ (Bankleitzahl) bank code. IBAN has been mandatory for all domestic and cross-border SEPA transfers since 2014.',
'FR': 'France has a highly developed banking system with major institutions like BNP Paribas, Societe Generale, and Credit Agricole. French IBANs are 27 characters long and include a 5-digit bank code (code banque), 5-digit branch code (code guichet), and an 11-digit account number. IBAN is required for all SEPA transfers in France.',
'GB': 'The United Kingdom uses a 22-character IBAN that incorporates the 6-digit sort code and 8-digit account number. Major UK banks include HSBC, Barclays, Lloyds, and NatWest. Despite leaving the EU, the UK remains part of SEPA, and IBAN is required for international transfers. The Faster Payments system handles domestic transfers without IBAN.',
'ES': 'Spain\'s banking system features major banks like Banco Santander, BBVA, and CaixaBank. Spanish IBANs are 24 characters and contain a 4-digit bank code, 4-digit branch code, 2-digit control code, and 10-digit account number. IBAN has been mandatory for all SEPA transfers in Spain since 2014.',
'IT': 'Italy has over 400 banks, led by UniCredit, Intesa Sanpaolo, and Banco BPM. Italian IBANs are 27 characters with a 5-digit bank code (ABI), 5-digit branch code (CAB), and 12-digit account number. The CIN (Control Internal Number) check character precedes the BBAN.',
'NL': 'The Netherlands has a modern banking system dominated by ING, Rabobank, and ABN AMRO. Dutch IBANs are 18 characters — the shortest in the EU — and use the bank\'s BIC-derived bank code. The Netherlands was an early adopter of IBAN and has fully transitioned away from the old domestic account numbering system.',
'CH': 'Switzerland is a global banking hub with major institutions like UBS, Credit Suisse (now part of UBS), and Raiffeisen. Swiss IBANs are 21 characters with a 5-digit bank clearing number and a 12-digit account identifier. Though not an EU member, Switzerland participates in SEPA and requires IBAN for cross-border transfers.',
'AT': 'Austria has a well-established banking system with Erste Group, Raiffeisen Bank International, and Bank Austria. Austrian IBANs are 20 characters and include a 5-digit bank code (BLZ) and an 11-digit account number. IBAN is mandatory for all SEPA transfers.',
'BE': 'Belgium has a sophisticated banking sector with KBC, BNP Paribas Fortis, and Belfius. Belgian IBANs are 16 characters with a 3-digit bank code and a 7+2 digit account number structure. Belgium was one of the first countries to mandate IBAN for domestic transfers.',
'PL': 'Poland has a well-regulated banking system with PKO Bank Polski, Pekao, and Santander Bank Polska. Polish IBANs are 28 characters — one of the longest in Europe — with an 8-digit bank sort code (including a 3-digit branch code) and a 16-digit account number.',
'SE': 'Sweden has a modern banking sector with Swedbank, SEB, Nordea, and Handelsbanken. Swedish IBANs are 24 characters with a 3-digit bank code and a 17-digit account number. Sweden has largely transitioned to digital payments, with bankgiro and plusgiro systems for domestic transfers.',
'DK': 'Denmark has a well-developed banking system with Danske Bank, Nordea, and Jyske Bank. Danish IBANs are 18 characters with a 4-digit bank code and a 10-digit account number. Denmark uses the PBS (Payment Business Services) system for domestic clearing.',
'NO': 'Norway has a robust banking sector with DNB, Sparebanken, and Nordea. Norwegian IBANs are 15 characters — among the shortest — with a 4-digit bank code and a 6-digit account number. Norway participates in SEPA despite not being an EU member.',
'FI': 'Finland has a highly digitised banking system with Nordea, OP Financial Group, and Danske Bank. Finnish IBANs are 18 characters with a 6-digit bank/branch code and a 7-8 digit account number. Finland was an early IBAN adopter.',
'PT': 'Portugal has a well-structured banking system with Caixa Geral de Depósitos, Millennium BCP, and Novo Banco. Portuguese IBANs are 25 characters with a 4-digit bank code, 4-digit branch code, and 11-digit account number. IBAN is mandatory for all SEPA transfers.',
'IE': 'Ireland has a modern banking sector with Allied Irish Banks (AIB), Bank of Ireland, and Permanent TSB. Irish IBANs are 22 characters with a 4-digit bank sort code and an 8-digit account number. Ireland uses IBAN for all SEPA transfers.',
'BR': 'Brazil has the largest banking sector in Latin America, with Itaú Unibanco, Banco do Brasil, Bradesco, and Santander Brasil. Brazilian IBANs are 29 characters with an 8-digit bank code (ISPB), 5-digit branch code, and 10-digit account number. IBAN is used for international transfers alongside the domestic PIX instant payment system.',
'TR': 'Turkey has a growing banking sector with Ziraat Bankası, İş Bankası, and Garanti BBVA. Turkish IBANs are 26 characters with a 5-digit bank code and a 16-digit account number. IBAN has been mandatory in Turkey since 2010.',
'AE': 'The United Arab Emirates has a well-capitalised banking system with Emirates NBD, First Abu Dhabi Bank, and Dubai Islamic Bank. UAE IBANs are 23 characters with a 3-digit bank code and a 14-digit account number. IBAN is mandatory for all cross-border transfers.',
'SA': 'Saudi Arabia has a strong banking sector with Saudi National Bank (SNB), Al Rajhi Bank, and Riyad Bank. Saudi IBANs are 24 characters with a 2-digit bank code and a variable-length account identifier. IBAN is mandatory for all domestic and international transfers.',
'HU': 'Hungary has a stable banking system with OTP Bank, K&H Bank, and Erste Bank Hungary. Hungarian IBANs are 28 characters with a 3-digit bank code, 4-digit branch code, and a 16+1 digit account number. IBAN is required for SEPA transfers.',
'CZ': 'The Czech Republic has a developed banking sector with Česká spořitelna, ČSOB, and Komerční banka. Czech IBANs are 24 characters with a 4-digit bank code and a 16-digit account number. IBAN is mandatory for SEPA transfers.',
'RO': 'Romania has a growing banking sector with Banca Transilvania, BCR, and BRD. Romanian IBANs are 24 characters with a 4-digit bank code and a 16-digit account number. IBAN is mandatory for all SEPA transfers.',
'GR': 'Greece has a well-established banking system with National Bank of Greece, Eurobank, Alpha Bank, and Piraeus Bank. Greek IBANs are 27 characters with a 3-digit bank code, 4-digit branch code, and a 16-digit account number.',
'BG': 'Bulgaria has a stable banking system with UniCredit Bulbank, DSK Bank, and United Bulgarian Bank. Bulgarian IBANs are 22 characters with a 4-digit bank code (BIC-based), 4-digit branch code, and a 10-digit account number.',
'HR': 'Croatia has a modern banking system with Zagrebačka banka, Privredna banka Zagreb, and Erste Bank Croatia. Croatian IBANs are 21 characters. Croatia adopted the euro in January 2023, making IBAN mandatory for all SEPA transfers.',
'LU': 'Luxembourg is a major international financial centre with BGL BNP Paribas, Banque Internationale à Luxembourg, and numerous private banks. Luxembourg IBANs are 20 characters with a 3-digit bank code and a 13-digit account number.',
'MT': 'Malta has a growing financial sector with Bank of Valletta, HSBC Malta, and APS Bank. Maltese IBANs are 31 characters with a 4-digit bank code, 5-digit branch code, and an 18-digit account number.',
'CY': 'Cyprus has a well-developed banking system with Bank of Cyprus, Hellenic Bank, and AstroBank. Cypriot IBANs are 28 characters with a 3-digit bank code, 5-digit branch code, and a 16-digit account number.',
'IS': 'Iceland has a small but robust banking system with Íslandsbanki, Landsbankinn, and Arion Bank. Icelandic IBANs are 26 characters with a 4-digit bank code and an 18-digit account number.',
'LI': 'Liechtenstein has a specialised financial centre with LGT Group, VP Bank, and Liechtensteinische Landesbank. Liechtenstein IBANs are 21 characters. The country uses the Swiss franc and participates in SEPA.',
'SG': 'Singapore is a global financial hub with DBS, OCBC, and UOB. Singapore does not officially use IBAN — international transfers use SWIFT/BIC codes with domestic account numbers.',
'HK': 'Hong Kong is a major international financial centre with HSBC, Bank of China (Hong Kong), and Standard Chartered. Hong Kong does not use IBAN — international transfers use SWIFT/BIC codes with domestic account numbers.',
'JP': 'Japan has a large banking system with MUFG Bank, Sumitomo Mitsui Banking Corporation, and Mizuho. Japan does not use IBAN — international transfers use SWIFT/BIC codes with domestic account numbers.',
'AU': 'Australia has a well-regulated banking system with Commonwealth Bank, Westpac, ANZ, and NAB. Australia does not use IBAN — domestic transfers use BSB (Bank State Branch) codes, and international transfers use SWIFT/BIC codes.',
'CA': 'Canada has a stable banking system with RBC, TD, Scotiabank, BMO, and CIBC. Canada does not use IBAN — international transfers use SWIFT/BIC codes with domestic transit and account numbers.',
'US': 'The United States has the world\'s largest banking system with JPMorgan Chase, Bank of America, Citibank, and Wells Fargo. The US does not use IBAN — domestic transfers use ABA routing numbers, and international transfers use SWIFT/BIC codes.',
}
def _banking_overview(c):
"""Generate a banking system overview paragraph for a country page."""
code = c['code']
name = c['name']
if code in BANKING_NOTES:
return '<p>{}</p>\n'.format(BANKING_NOTES[code])
# Generic fallback based on SEPA status and continent
parts = []
if c['sepa']:
parts.append('{} is a SEPA member country.'.format(name))
parts.append('All SEPA credit transfers and direct debits in {} require a valid IBAN.'.format(name))
else:
parts.append('{} uses the IBAN system for international bank transfers.'.format(name))
parts.append('The {} IBAN is {} characters long and consists of the ISO country code "{}", two MOD-97 check digits, and a Basic Bank Account Number (BBAN) containing the domestic bank code ({} digits), account number, and any branch/routing identifiers.'.format(
name, c['ibanLen'], code, c['bankLen']))
return '<p>{}</p>\n'.format(' '.join(parts))
# ── Helpers ──────────────────────────────────────────────────────
def page(dirname, content):
"""Write content to src/<dirname>/index.html, creating dirs as needed."""
d = os.path.join(SRC, dirname)
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, 'index.html'), 'w', encoding='utf-8') as f:
f.write(content)
def esc(s):
"""Basic HTML entity escape."""
return s.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"')
def slug(name):
"""Convert country name to URL slug."""
s = name.lower().strip()
s = s.replace('&', 'and')
s = re.sub(r'[^a-z0-9]+', '-', s)
return s.strip('-')
def continent_order(name):
"""Sort continents in common order."""
order = {'Europe': 0, 'North America': 1, 'South America': 2, 'Asia': 3, 'Africa': 4, 'Oceania': 5}
return order.get(name, 9)
# ── IBAN Structure Diagram ───────────────────────────────────────
def build_structure_diagram(c):
"""Generate the visual IBAN structure block diagram."""
parts = []
parts.append('<div class="iban-legend">')
parts.append('<span><span class="swatch" style="background:var(--accent-subtle);border-color:var(--accent-light)"></span> Country ({})</span>'.format(c['code']))
parts.append('<span><span class="swatch" style="background:#2ed5731a;border-color:var(--green)"></span> Check digits</span>')
parts.append('<span><span class="swatch" style="background:#ffc94a1a;border-color:var(--gold)"></span> Bank code ({} {})</span>'.format(c['bankLen'], 'letters' if c['bbanFormat'].startswith(str(c['bankLen'])+'!a') else 'digits'))
if c.get('branchStart') and c.get('branchLen'):
parts.append('<span><span class="swatch" style="background:#a855f71a;border-color:var(--purple)"></span> Branch ({} chars)</span>'.format(c['branchLen']))
parts.append('<span><span class="swatch" style="background:var(--bg-elev-1);border-color:var(--text-muted)"></span> Account ({} chars)</span>'.format(c['accountLen']))
parts.append('</div>')
parts.append('<div class="iban-structure">')
# Country
parts.append('<div class="iban-block country" title="Country code">{}</div>'.format(c['code']))
# Check digits
parts.append('<div class="iban-block check" title="Check digits">XX</div>')
# Bank
bank_text = 'Bank' if c['bankLen'] <= 6 else 'Bank code'
parts.append('<div class="iban-block bank" title="Bank code ({} chars)">{}<br><small>{} chars</small></div>'.format(c['bankLen'], bank_text, c['bankLen']))
# Branch (if applicable)
if c.get('branchStart') and c.get('branchLen'):
parts.append('<div class="iban-block branch" title="Branch code">Branch<br><small>{} chars</small></div>'.format(c['branchLen']))
# Account
acc_text = 'Account' if c['accountLen'] <= 10 else 'Account number'
parts.append('<div class="iban-block account" title="Account number ({} chars)">{}<br><small>{} chars</small></div>'.format(c['accountLen'], acc_text, c['accountLen']))
parts.append('</div>')
return '\n'.join(parts)
# ── Spec Table ───────────────────────────────────────────────────
def build_spec_table(c):
rows = [
('IBAN Length', '{} characters'.format(c['ibanLen'])),
('BBAN Length', '{} characters'.format(c['bbanLen'])),
('BBAN Format', '<code>{}</code>'.format(c['bbanFormat'])),
('Bank Code', 'Position {}, {} {}'.format(c['bankStart'], c['bankLen'], 'letters' if re.match(r'\d+!a', c['bbanFormat']) else 'digits')),
]
if c.get('branchStart') and c.get('branchLen'):
rows.append(('Branch Code', 'Position {}, {} chars'.format(c['branchStart'], c['branchLen'])))
rows.append(('Account Number', 'Position {}, {} chars'.format(c['accountStart'], c['accountLen'])))
rows.append(('SEPA Member', '✅ Yes' if c['sepa'] else '❌ No'))
if c.get('notes'):
rows.append(('Notes', c['notes']))
html = '<div class="tablewrap"><table>\n<thead><tr><th>Property</th><th>Value</th></tr></thead>\n<tbody>\n'
for label, val in rows:
html += '<tr><td>{}</td><td>{}</td></tr>\n'.format(label, val)
html += '</tbody>\n</table></div>'
return html
# ── Example IBANs ────────────────────────────────────────────────
def build_examples(c, count=3):
examples = []
for _ in range(count):
iban = generate_iban(c)
examples.append(format_iban(iban))
return examples
# ── Country-specific FAQs ────────────────────────────────────────
def country_faqs(c):
name = c['name']
return [
('What is the IBAN format for {}?'.format(name),
'{} uses a {}-character IBAN with the country code "{}" followed by two check digits and a {}-character BBAN. The format is <code>{}</code>. The bank code occupies positions {} to {}.'.format(
name, c['ibanLen'], c['code'], c['bbanLen'], c['bbanFormat'],
c['bankStart'], c['bankStart'] + c['bankLen'] - 1)),
('Is {} a SEPA country?'.format(name),
'Yes, {} is part of the SEPA (Single Euro Payments Area) and uses IBAN for all domestic and international transfers.'.format(name) if c['sepa'] else
'No, {} is not a SEPA member, but uses IBAN for international transfers.'.format(name)),
('How long is a {} IBAN?'.format(name),
'A {} IBAN is exactly {} characters long. The BBAN portion accounts for {} characters.'.format(name, c['ibanLen'], c['bbanLen'])),
('How do I find my {} IBAN?'.format(name),
'Your IBAN is printed on your bank statement, available in your online banking portal, or can be obtained from your bank. It typically appears alongside your BIC/SWIFT code for international transfers.'),
('Can I generate a valid {} IBAN for testing?'.format(name),
'Yes! Use the IBAN generator on this page or visit the <a href="/">IBAN Easy homepage</a> to generate mathematically valid IBANs for any supported country. All generation is client-side and private.'),
]
# ── Page Builders ────────────────────────────────────────────────
def build_country_page(c):
"""Build a single country detail page."""
name = c['name']
code = c['code']
can_path = '/countries/{}/'.format(code.lower())
title = '{} IBAN Generator — Format, Structure & Examples'.format(name)
desc = 'Generate valid {} IBANs. {} IBAN format: {}-character code starting with {}, bank code ({}) + account number. SEPA member: {}. Free online tool.'.format(
name, name, c['ibanLen'], code, c['bankLen'], 'Yes' if c['sepa'] else 'No')
extra = schema_script({
'@context': 'https://schema.org',
'@type': 'FAQPage',
'mainEntity': [{'@type': 'Question', 'name': q, 'acceptedAnswer': {'@type': 'Answer', 'text': a}} for q, a in country_faqs(c)]
}) + schema_script(breadcrumb_schema([
('Home', SITE + '/'),
('Countries', SITE + '/countries/'),
('{} IBAN'.format(name), SITE + can_path),
]))
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + can_path,
og_title=esc(title), og_desc=esc(desc),
extra=extra
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><a href="/countries/">Countries</a><span class="crumb-sep">/</span><span class="crumb-current">{}</span></nav>\n'.format(name)
body += '<main class="main-content prose-content">\n'
body += '<h1>{} IBAN Format & Generator</h1>\n'.format(esc(name))
body += '<p>Complete reference for {} IBANs: format structure, bank codes, account numbers, examples, and a free generator for testing.</p>\n'.format(name)
# Structure diagram
body += '<h2>IBAN Structure for {}</h2>\n'.format(name)
body += build_structure_diagram(c)
# Specs table
body += '<h2>{} IBAN Technical Specifications</h2>\n'.format(name)
body += build_spec_table(c)
# Banking system overview
body += '<h2>Banking System in {}</h2>\n'.format(name)
body += _banking_overview(c)
# Examples
examples = build_examples(c, 3)
body += '<h2>Example {} IBANs</h2>\n'.format(name)
body += '<p>Here are 3 randomly generated but <strong>mathematically valid</strong> {} IBANs for testing:</p>\n'.format(name)
body += '<ul>\n'
for ex in examples:
body += '<li><code>{}</code></li>\n'.format(ex)
body += '</ul>\n'
body += '<p style="font-size:0.85rem;color:var(--text-muted)">⚠ These are randomly generated test IBANs — not real bank accounts. Do not use for actual transactions.</p>\n'
# In-page mini generator
body += '<h2>Generate {} IBANs for Testing</h2>\n'.format(name)
body += '<p>Generate valid {} IBANs right here. All generation is client-side — no data leaves your browser.</p>\n'.format(name)
body += '<div class="glass-panel" style="text-align:center">\n'
body += '<div class="quantity-row" style="max-width:320px;margin:0 auto 0.85rem">\n'
body += '<label for="mini-qty">Quantity</label>\n'
body += '<div class="quantity-control">\n'
body += '<input type="range" id="mini-qty" min="1" max="100" value="5">\n'
body += '<span class="quantity-num" id="mini-qty-val">5</span>\n'
body += '</div></div>\n'
body += '<button class="btn btn-primary" id="mini-gen" style="margin-bottom:0.85rem">↻ Generate {} IBANs</button>\n'.format(name)
body += '<div id="mini-results" style="text-align:left;max-height:300px;overflow-y:auto"></div>\n'
body += '<div style="margin-top:0.75rem;display:none" id="mini-actions">\n'
body += '<button class="btn btn-ghost btn-sm" id="mini-copy-all">⎘ Copy All</button>\n'
body += '<button class="btn btn-ghost btn-sm" id="mini-csv">CSV</button>\n'
body += '<button class="btn btn-ghost btn-sm" id="mini-json">JSON</button>\n'
body += '<button class="btn btn-ghost btn-sm" id="mini-txt">TXT</button>\n'
body += '</div></div>\n'
body += '<script src="/js/iban-core.js"></script>\n'
body += '<script>\n'
body += '(function() {\n'
body += ' var COUNTRY = "{}";\n'.format(code)
body += ' var FORMAT = "{}";\n'.format(c['bbanFormat'])
body += ' var results = [];\n'
body += ' var qty = document.getElementById("mini-qty");\n'
body += ' var qtyVal = document.getElementById("mini-qty-val");\n'
body += ' qty.addEventListener("input", function() { qtyVal.textContent = qty.value; });\n'
body += ' document.getElementById("mini-gen").addEventListener("click", function() {\n'
body += ' var n = parseInt(qty.value, 10);\n'
body += ' results = [];\n'
body += ' var html = "";\n'
body += ' for (var i = 0; i < n; i++) {\n'
body += ' var iban = IBAN.generate(COUNTRY, FORMAT);\n'
body += ' var fmt = IBAN.format(iban);\n'
body += ' results.push({raw: iban, formatted: fmt});\n'
body += ' html += "<div style=\\"font-family:var(--font-display);padding:0.35rem 0;border-bottom:1px solid #ffffff08;font-size:0.88rem\\">" + fmt + "</div>";\n'
body += ' }\n'
body += ' document.getElementById("mini-results").innerHTML = html;\n'
body += ' document.getElementById("mini-actions").style.display = "";\n'
body += ' });\n'
body += ' function download(filename, content, mime) {\n'
body += ' var b = new Blob([content], {type: mime + ";charset=utf-8"});\n'
body += ' var u = URL.createObjectURL(b);\n'
body += ' var a = document.createElement("a");\n'
body += ' a.href = u; a.download = filename;\n'
body += ' document.body.appendChild(a); a.click();\n'
body += ' document.body.removeChild(a); URL.revokeObjectURL(u);\n'
body += ' }\n'
body += ' function copyAll() {\n'
body += ' var t = results.map(function(r) { return r.formatted; }).join("\\n");\n'
body += ' navigator.clipboard.writeText(t);\n'
body += ' }\n'
body += ' document.getElementById("mini-copy-all").addEventListener("click", copyAll);\n'
body += ' document.getElementById("mini-csv").addEventListener("click", function() {\n'
body += ' download("ibans.csv", "IBAN\\n" + results.map(function(r) { return r.raw; }).join("\\n"), "text/csv");\n'
body += ' });\n'
body += ' document.getElementById("mini-json").addEventListener("click", function() {\n'
body += ' download("ibans.json", JSON.stringify(results.map(function(r) { return {iban: r.raw, formatted: r.formatted}; }), null, 2), "application/json");\n'
body += ' });\n'
body += ' document.getElementById("mini-txt").addEventListener("click", function() {\n'
body += ' download("ibans.txt", results.map(function(r) { return r.raw; }).join("\\n"), "text/plain");\n'
body += ' });\n'
body += '})();\n'
body += '</script>\n'
body += '<h2>Frequently Asked Questions</h2>\n'
body += '<div class="faq-list">\n'
for q, a in country_faqs(c):
body += '<details class="faq-item"><summary class="faq-q">{}</summary><div class="faq-a"><p>{}</p></div></details>\n'.format(q, a)
body += '</div>\n'
# Related countries
continent = c['continent']
related = [x for x in countries if x['continent'] == continent and x['code'] != code][:8]
if related:
body += '<h2>Other IBAN Countries in {}</h2>\n'.format(continent)
body += '<ul>\n'
for r in related:
body += '<li><a href="/countries/{}/">{} IBAN ({} chars{})</a></li>\n'.format(
r['code'].lower(), r['name'], r['ibanLen'],
', SEPA' if r['sepa'] else '')
body += '</ul>\n'
# BIC/SWIFT codes section for this country
bic_file = os.path.join(ROOT, 'swift_codes.json')
if os.path.exists(bic_file):
with open(bic_file, 'r', encoding='utf-8') as f:
bic_data = json.load(f)
country_bics = [item for item in bic_data if item['country'] == code]
if country_bics:
body += '<h2>Major Bank BIC/SWIFT Codes in {}</h2>\n'.format(name)
body += '<p>BIC (Bank Identifier Code) / SWIFT codes for major banks in {}. These codes are used for international wire transfers alongside the IBAN.</p>\n'.format(name)
body += '<div class="tablewrap"><table>\n'
body += '<thead><tr><th>BIC Code</th><th>Bank Name</th><th>City</th></tr></thead>\n<tbody>\n'
for b in sorted(country_bics, key=lambda x: x['bankName'])[:25]:
body += '<tr><td><code class="swift-bic-code">{}</code></td><td>{}</td><td>{}</td></tr>\n'.format(
esc(b['bic']), esc(b['bankName']), esc(b.get('city', '—')))
body += '</tbody>\n</table></div>\n'
if len(country_bics) > 25:
body += '<p style="font-size:0.85rem;color:var(--text-muted)">Showing 25 of {} banks. <a href="/swift-codes/">Search all SWIFT/BIC codes →</a></p>\n'.format(len(country_bics))
# Related tools & resources (internal linking)
body += '<h2>IBAN Tools & Resources</h2>\n'
body += '<div class="card-grid card-grid-2" style="margin-top:0.5rem">\n'
body += '<a class="country-card" href="/iban-calculator/"><div class="cc-badge">🔢</div><div class="cc-name">IBAN Calculator</div><div class="cc-meta">Compute check digits for {} IBANs from domestic account details</div></a>\n'.format(name)
body += '<a class="country-card" href="/validate/"><div class="cc-badge">✔</div><div class="cc-name">IBAN Validator</div><div class="cc-meta">Check any IBAN (including {} IBANs) with MOD-97</div></a>\n'.format(name)
body += '<a class="country-card" href="/what-is-iban/"><div class="cc-badge">❓</div><div class="cc-name">What is an IBAN?</div><div class="cc-meta">Complete guide to IBAN structure and how it works</div></a>\n'
body += '<a class="country-card" href="/learn/iban-vs-swift/"><div class="cc-badge">📨</div><div class="cc-name">IBAN vs SWIFT</div><div class="cc-meta">When do you need an IBAN, a SWIFT code, or both?</div></a>\n'
body += '<a class="country-card" href="/learn/how-to-get-an-iban/"><div class="cc-badge">📍</div><div class="cc-name">How to Find Your IBAN</div><div class="cc-meta">5 ways to locate your {} IBAN quickly</div></a>\n'.format(name)
body += '<a class="country-card" href="/swift-codes/"><div class="cc-badge">📨</div><div class="cc-name">SWIFT/BIC Lookup</div><div class="cc-meta">Find bank BIC codes for international transfers</div></a>\n'
body += '</div>\n'
body += FOOT
return body
def build_countries_index():
"""Build the /countries/ index page listing all countries."""
title = 'All IBAN Countries — Complete List of 96 IBAN Formats Worldwide'
desc = 'Browse all 96 countries that use IBAN. Filter by continent and SEPA membership. Each country page includes IBAN format, bank code details, examples, and a free generator.'
extra = schema_script(itemlist_schema([
(c['name'], SITE + '/countries/{}/'.format(c['code'].lower()))
for c in sorted(countries, key=lambda x: x['name'])
])) + schema_script(breadcrumb_schema([
('Home', SITE + '/'),
('Countries', SITE + '/countries/'),
]))
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + '/countries/',
og_title=esc(title), og_desc=esc(desc),
extra=extra
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><span class="crumb-current">Countries</span></nav>\n'
body += '<main class="main-content">\n'
body += '<h1>All IBAN Countries</h1>\n'
body += '<p>96 countries and territories worldwide use the IBAN system. Browse by continent or SEPA membership. Click any country for full IBAN format details, examples, and a generator.</p>\n'
# Search
body += '<input type="text" class="search-box" id="country-search" placeholder="Search countries..." oninput="filterCountries()" aria-label="Search countries">\n'
# Stats
sepa_count = sum(1 for c in countries if c['sepa'])
body += '<p class="text-secondary" style="margin-bottom:1.5rem"><strong style="color:var(--text)">{}</strong> SEPA countries | <strong style="color:var(--text)">{}</strong> non-SEPA IBAN countries | <strong style="color:var(--text)">{}</strong> total</p>\n'.format(
sepa_count, len(countries) - sepa_count, len(countries))
# Group by continent
continents = {}
for c in countries:
cont = c['continent']
if cont not in continents:
continents[cont] = []
continents[cont].append(c)
for cont in sorted(continents, key=continent_order):
body += '<h2>{}</h2>\n'.format(cont)
body += '<div class="card-grid card-grid-4">\n'
for c in sorted(continents[cont], key=lambda x: x['name']):
badge = ' <span class="tag tag-green">SEPA</span>' if c['sepa'] else ''
body += '<a class="country-card" href="/countries/{}/"><div class="cc-badge">{}</div><div class="cc-name">{} {}</div><div class="cc-meta">{} chars</div></a>\n'.format(
c['code'].lower(), c['code'], c['name'], badge, c['ibanLen'])
body += '</div>\n'
# Search script
body += '''<script>
function filterCountries() {
var q = document.getElementById('country-search').value.toLowerCase();
var sections = document.querySelectorAll('h2');
var grids = document.querySelectorAll('.card-grid');
for (var i = 0; i < grids.length; i++) {
var cards = grids[i].querySelectorAll('.country-card');
var visible = 0;
for (var j = 0; j < cards.length; j++) {
var text = cards[j].textContent.toLowerCase();
if (text.indexOf(q) !== -1) { cards[j].style.display = ''; visible++; }
else { cards[j].style.display = 'none'; }
}
if (sections[i+1]) sections[i+1].style.display = visible > 0 ? '' : 'none';
if (grids[i].previousElementSibling && grids[i].previousElementSibling.tagName === 'H2') {
grids[i].previousElementSibling.style.display = visible > 0 ? '' : 'none';
}
}
}
</script>'''
body += FOOT
return body
def build_validator_page():
"""Build the /validate/ standalone validator page."""
title = 'Free IBAN Validator — Check Any IBAN Number Online'
desc = 'Validate any IBAN number instantly. Checks country code, length, and MOD-97 check digits. Supports all 96 IBAN countries. 100% client-side — your data stays private.'
extra = schema_script({
'@context': 'https://schema.org',
'@type': 'WebApplication',
'name': 'IBAN Validator',
'url': SITE + '/validate/',
'description': 'Free online IBAN validator — check any IBAN for correctness using MOD-97 algorithm',
'applicationCategory': 'FinanceApplication'
}) + schema_script(breadcrumb_schema([
('Home', SITE + '/'),
('IBAN Validator', SITE + '/validate/'),
]))
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + '/validate/',
og_title=esc(title), og_desc=esc(desc),
extra=extra
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><span class="crumb-current">Validator</span></nav>\n'
body += '<main class="main-content prose-content">\n'
body += '<h1>IBAN Validator</h1>\n'
body += '<p>Enter any IBAN to check if it\'s valid. We verify the country code, total length, character format, and MOD-97 check digits.</p>\n'
body += '<input type="text" class="validator-input" id="iban-input" placeholder="Enter IBAN (e.g., DE89 3704 0044 0532 0130 00)" autocomplete="off" aria-label="Enter IBAN to validate">\n'
body += '<div class="validator-result" id="result" style="display:none"></div>\n'
body += '<div style="margin-top:1rem" id="details" style="display:none"></div>\n'
body += '\n'
body += '<h2>How IBAN Validation Works</h2>\n'
body += '<p>IBAN validation involves three checks:</p>\n'
body += '<ol>\n'
body += '<li><strong>Format check</strong>: The IBAN must start with two letters (country code), followed by two digits (check digits), then 11-30 alphanumeric characters. Total length must match the country\'s expected length.</li>\n'
body += '<li><strong>Country check</strong>: The first two characters must be a valid ISO country code that participates in the IBAN system.</li>\n'
body += '<li><strong>MOD-97 check</strong>: The IBAN is rearranged (move first 4 chars to end), letters are converted to numbers (A=10, B=11, ..., Z=35), and the resulting huge integer must give remainder 1 when divided by 97.</li>\n'
body += '</ol>\n'
body += '<p><a href="/iban-check-digit/">Learn more about the MOD-97 algorithm →</a></p>\n'
body += '''<script src="/js/iban-core.js"></script>
<script>
(function() {
var inp = document.getElementById('iban-input');
var result = document.getElementById('result');
var details = document.getElementById('details');
// Country length lookup (populated from the same data)
var COUNTRY_LENGTHS = {};
var COUNTRY_NAMES = {};
var xhr = new XMLHttpRequest();
xhr.open('GET', '/iban_countries.json', true);
xhr.onload = function() {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
for (var i = 0; i < data.length; i++) {
COUNTRY_LENGTHS[data[i].code] = data[i].ibanLen;
COUNTRY_NAMES[data[i].code] = data[i].name;
}
}
};
xhr.send();
function check() {
var val = inp.value.trim();
if (!val) { result.style.display = 'none'; details.style.display = 'none'; return; }
var clean = IBAN.sanitise(val);
var info = [];
// Check 1: Country exists
var cc = clean.slice(0, 2);
if (COUNTRY_LENGTHS[cc]) {
info.push('Country: ' + COUNTRY_NAMES[cc] + ' (' + cc + ')');
// Check 2: Length
var expectedLen = COUNTRY_LENGTHS[cc];
if (clean.length !== expectedLen) {
info.push('Length: ' + clean.length + ' characters (expected ' + expectedLen + ' for ' + COUNTRY_NAMES[cc] + ')');
inp.className = 'validator-input invalid';
result.style.display = 'block';
result.className = 'validator-result invalid';
result.textContent = '\\u2718 Invalid — Wrong length for ' + COUNTRY_NAMES[cc];
details.innerHTML = '<div class="tablewrap"><table>' + info.map(function(i) { return '<tr><td>' + i + '</td></tr>'; }).join('') + '</table></div>';
details.style.display = 'block';
return;
}
info.push('Length: ' + clean.length + ' characters (correct for ' + COUNTRY_NAMES[cc] + ')');
} else {
info.push('Country code "' + cc + '" is not a valid IBAN country');
}
// Check 3: MOD-97
var valid = IBAN.isValid(val);
info.push('MOD-97 check: ' + (valid ? 'Passed \\u2705' : 'Failed \\u274C'));
if (valid) {
inp.className = 'validator-input valid';
result.style.display = 'block';
result.className = 'validator-result valid';
result.textContent = '\\u2714 Valid IBAN — ' + (COUNTRY_NAMES[cc] || cc);
} else {
inp.className = 'validator-input invalid';
result.style.display = 'block';
result.className = 'validator-result invalid';
result.textContent = '\\u2718 Invalid IBAN';
}
details.innerHTML = '<div class="tablewrap"><table>' + info.map(function(i) { return '<tr><td>' + i + '</td></tr>'; }).join('') + '</table></div>';
details.style.display = 'block';
}
inp.addEventListener('input', check);
inp.addEventListener('paste', function() { setTimeout(check, 50); });
})();
</script>'''
body += FOOT
return body
def build_sepa_page():
"""Build the /sepa-countries/ page."""
title = 'SEPA Countries — Complete List of 36 SEPA IBAN Countries (2026)'
desc = 'Full list of all 36 SEPA countries and their IBAN formats. SEPA includes EU/EEA states, Switzerland, Monaco, San Marino, Andorra, Vatican City, and UK crown dependencies.'
sepa_countries = [c for c in countries if c['sepa']]
extra = schema_script(itemlist_schema([
(c['name'], SITE + '/countries/{}/'.format(c['code'].lower()))
for c in sorted(sepa_countries, key=lambda x: x['name'])
])) + schema_script(breadcrumb_schema([
('Home', SITE + '/'),
('SEPA Countries', SITE + '/sepa-countries/'),
]))
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + '/sepa-countries/',
og_title=esc(title), og_desc=esc(desc),
extra=extra
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><span class="crumb-current">SEPA Countries</span></nav>\n'
body += '<main class="main-content prose-content">\n'
body += '<h1>SEPA Countries List</h1>\n'
body += '<p>The Single Euro Payments Area (SEPA) includes 36 countries. All SEPA transfers require an IBAN. Here is the complete list with IBAN formats.</p>\n'
body += '\n'
sepa = [c for c in countries if c['sepa']]
body += '<h2>All {} SEPA Members</h2>\n'.format(len(sepa))
body += '<div class="tablewrap"><table>\n'
body += '<thead><tr><th>Country</th><th>Code</th><th>IBAN Length</th><th>Bank Code</th><th>Format</th></tr></thead>\n<tbody>\n'
for c in sorted(sepa, key=lambda x: x['name']):
body += '<tr><td><a href="/countries/{code}/">{name}</a></td><td><code>{code}</code></td><td>{ibanLen}</td><td>{bankLen} {bankType}</td><td><code>{fmt}</code></td></tr>\n'.format(
code=c['code'].lower(), name=c['name'], ibanLen=c['ibanLen'],
bankLen=c['bankLen'],
bankType='letters' if re.match(r'\d+!a', c['bbanFormat']) else 'digits',
fmt=c['bbanFormat'])
body += '</tbody>\n</table></div>\n'
body += '<h2>What is SEPA?</h2>\n'
body += '<p>The Single Euro Payments Area (SEPA) is a payment integration initiative of the European Union. It allows customers to make cashless euro payments to anywhere in the SEPA zone using a single bank account and IBAN.</p>\n'
body += '<ul>\n'
body += '<li><strong>EU/EEA countries</strong>: 31 members (all EU states plus Norway, Iceland, Liechtenstein)</li>\n'
body += '<li><strong>Non-EEA SEPA members</strong>: Switzerland, Monaco, San Marino, Andorra, Vatican City</li>\n'
body += '<li><strong>UK & Crown Dependencies</strong>: Though no longer EU members, UK, Guernsey, Jersey, and Isle of Man remain in SEPA</li>\n'
body += '</ul>\n'
body += FOOT
return body
def build_check_digit_page():
"""Build the /iban-check-digit/ educational page."""
title = 'IBAN Check Digits Explained — How MOD-97 Validates Your IBAN'
desc = 'Learn how IBAN check digits work using the MOD-97 algorithm. Step-by-step explanation with examples. Understand why IBANs are so reliable for international banking.'
extra = schema_script(breadcrumb_schema([
('Home', SITE + '/'),
('IBAN Check Digits', SITE + '/iban-check-digit/'),
]))
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + '/iban-check-digit/',
og_title=esc(title), og_desc=esc(desc),
extra=extra
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><span class="crumb-current">IBAN Check Digits</span></nav>\n'
body += '<main class="main-content prose-content">\n'
body += '<h1>How IBAN Check Digits Work</h1>\n'
body += '<p>Every IBAN includes two check digits that catch 99.9% of typing errors. They use a mathematical formula called MOD-97, first published in ISO 7064. Here\'s how it works.</p>\n'
body += '\n'
body += '<h2>The MOD-97 Algorithm</h2>\n'
body += '<p>The check digits (positions 3-4 in every IBAN) are computed using the MOD-97 algorithm. Here is the step-by-step process:</p>\n'
body += '<h3>Step 1: Rearrange the IBAN</h3>\n'
body += '<p>Move the first four characters (country code + check digits) to the end of the string.</p>\n'
body += '<p>Example for <code>GB29 NWBK 6016 1331 9268 19</code>:</p>\n'
body += '<pre>Original: GB29 NWBK 6016 1331 9268 19\nRearrange: NWBK 6016 1331 9268 19 GB29</pre>\n'
body += '<h3>Step 2: Convert Letters to Numbers</h3>\n'
body += '<p>Replace each letter with its numeric value: A=10, B=11, ..., Z=35.</p>\n'
body += '<pre>A=10, B=11, C=12, ..., Z=35\nN → 23, W → 32, B → 11, K → 20, G → 16, B → 11</pre>\n'
body += '<h3>Step 3: Compute MOD-97</h3>\n'
body += '<p>Interpret the resulting string as a huge integer and divide by 97. A valid IBAN always gives <strong>remainder 1</strong>.</p>\n'
body += '<pre>NWBK60161331926819GB29 → (huge integer) mod 97 = 1 ✓</pre>\n'
body += '<h2>How Check Digits Are Generated</h2>\n'
body += '<p>When a bank creates an IBAN, it:</p>\n'
body += '<ol>\n'
body += '<li>Starts with the country code + "00" + BBAN (domestic account details)</li>\n'
body += '<li>Moves the first 4 chars to the end</li>\n'
body += '<li>Converts letters to numbers</li>\n'
body += '<li>Computes <code>remainder = big_number mod 97</code></li>\n'
body += '<li>Check digits = <code>98 − remainder</code> (zero-padded to 2 digits)</li>\n'
body += '</ol>\n'
body += '<h2>Why 97?</h2>\n'
body += '<p>97 was chosen because:</p>\n'
body += '<ul>\n'
body += '<li>It\'s the largest two-digit prime number</li>\n'
body += '<li>It catches 99.94% of single-digit typos and transposition errors</li>\n'
body += '<li>The math works efficiently for numbers of any length</li>\n'
body += '</ul>\n'
body += '<h2>Computing MOD-97 in JavaScript</h2>\n'
body += '<p>Since IBAN numeric strings can be 30+ digits (too large for JavaScript\'s Number type), we process in chunks:</p>\n'
body += '<pre>function mod97(numStr) {\n let remainder = numStr;\n while (remainder.length > 2) {\n let chunk = remainder.slice(0, 9); // 9 digits safely < MAX_SAFE_INTEGER\n remainder = (parseInt(chunk, 10) % 97).toString() + remainder.slice(chunk.length);\n }\n return parseInt(remainder, 10) % 97;\n}</pre>\n'
body += '<p><a href="/">Try our IBAN generator →</a></p>\n'
body += FOOT
return body
def build_contact_page():
"""Build the /contact/ page."""
title = 'Contact IBAN Easy — Get in Touch'
desc = 'Contact the IBAN Easy team. Questions, feedback, or suggestions about our free IBAN generator and validator tools.'
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + '/contact/',
og_title=esc(title), og_desc=esc(desc),
extra=''
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><span class="crumb-current">Contact</span></nav>\n'
body += '<main class="main-content prose-content">\n'
body += '<h1>Contact Us</h1>\n'
body += '<p>Have a question, suggestion, or found an issue? We\'d love to hear from you.</p>\n'
body += '<h2>Email</h2>\n'
body += '<p>For general inquiries, bug reports, or feature requests, email us at:</p>\n'
body += '<p><strong>hello@ibaneasy.com</strong></p>\n'
body += '<h2>Response Time</h2>\n'
body += '<p>We aim to respond within 2-3 business days. For urgent issues, please include as much detail as possible in your email.</p>\n'
body += '<h2>Before You Write</h2>\n'
body += '<ul>\n'
body += '<li><strong>IBAN questions?</strong> Check our <a href="/iban-check-digit/">Check Digits guide</a> or <a href="/countries/">country pages</a>.</li>\n'
body += '<li><strong>Need test IBANs?</strong> Use our <a href="/">free generator</a> — no sign-up needed.</li>\n'
body += '<li><strong>Privacy concerns?</strong> Read our <a href="/privacy/">Privacy Policy</a> to understand how we handle data (spoiler: we don\'t collect any).</li>\n'
body += '</ul>\n'
body += FOOT
return body
def build_privacy_page():
"""Build the /privacy/ page."""
title = 'Privacy Policy — IBAN Easy'
desc = 'IBAN Easy privacy policy. We do not collect, store, or transmit any personal data. All IBAN generation and validation runs entirely in your browser.'
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + '/privacy/',
og_title=esc(title), og_desc=esc(desc),
extra=''
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><span class="crumb-current">Privacy Policy</span></nav>\n'
body += '<main class="main-content prose-content">\n'
body += '<h1>Privacy Policy</h1>\n'
body += '<p><em>Last updated: {}</em></p>\n'.format(TODAY)
body += '<h2>1. No Data Collection</h2>\n'
body += '<p>IBAN Easy does <strong>not</strong> collect, store, or transmit any personal data. All IBAN generation and validation is performed entirely in your browser using JavaScript. No data is ever sent to any server.</p>\n'
body += '<h2>2. No Cookies</h2>\n'
body += '<p>We do not use cookies for tracking or advertising. The only data stored locally is your generation history (stored in your browser\'s localStorage), which never leaves your device.</p>\n'
body += '<h2>3. No Analytics</h2>\n'
body += '<p>We do not use any third-party analytics services (no Google Analytics, no tracking pixels). We may review server-side request logs (anonymized) for security and performance purposes only.</p>\n'
body += '<h2>4. Third-Party Services</h2>\n'
body += '<p>Our site loads Google Fonts (Space Grotesk, Inter) for typography. Google may collect usage data according to their own privacy policy when these fonts are loaded. No other third-party services are used.</p>\n'
body += '<h2>5. Security</h2>\n'
body += '<p>Since all processing happens client-side, there is no risk of a data breach exposing your information — we simply never have it. The site is served over HTTPS via Cloudflare.</p>\n'
body += '<h2>6. Contact</h2>\n'
body += '<p>For any privacy-related questions, contact us at <strong>hello@ibaneasy.com</strong>.</p>\n'
body += FOOT
return body
def build_terms_page():
"""Build the /terms/ page."""
title = 'Terms of Use — IBAN Easy'
desc = 'Terms of use for IBAN Easy. Our IBAN generator and validator are free tools for testing and development. Read our terms before using the service.'
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + '/terms/',
og_title=esc(title), og_desc=esc(desc),
extra=''
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><span class="crumb-current">Terms of Use</span></nav>\n'
body += '<main class="main-content prose-content">\n'
body += '<h1>Terms of Use</h1>\n'
body += '<p><em>Last updated: {}</em></p>\n'.format(TODAY)
body += '<h2>1. Acceptance of Terms</h2>\n'
body += '<p>By using ibaneasy.com, you agree to these terms. If you do not agree, please do not use the site.</p>\n'
body += '<h2>2. Service Description</h2>\n'
body += '<p>IBAN Easy provides a free online IBAN generator and validator for testing and development purposes. The service is provided "as is" without any warranty.</p>\n'
body += '<h2>3. No Financial Advice</h2>\n'
body += '<p>This website does not provide financial advice. The IBANs generated are <strong>mathematically valid but not real bank accounts</strong>. They must not be used for actual financial transactions, payments, or any production system involving real money.</p>\n'
body += '<h2>4. Disclaimer</h2>\n'
body += '<p>IBAN Easy makes no guarantees about the accuracy, completeness, or suitability of the generated data. We are not responsible for any damages resulting from the use of generated IBANs. Users are responsible for verifying any IBAN data against production bank records.</p>\n'
body += '<h2>5. Acceptable Use</h2>\n'
body += '<p>You agree not to:</p>\n'
body += '<ul>\n'
body += '<li>Use generated IBANs for fraud, money laundering, or any illegal activity</li>\n'
body += '<li>Attempt to use generated IBANs for real bank transfers</li>\n'
body += '<li>Scrape, overload, or attempt to disrupt the service</li>\n'
body += '</ul>\n'
body += '<h2>6. Changes to Terms</h2>\n'
body += '<p>We may update these terms at any time. Continued use of the site after changes constitutes acceptance of the new terms.</p>\n'
body += '<h2>7. Contact</h2>\n'
body += '<p>Questions about these terms? Email <strong>hello@ibaneasy.com</strong>.</p>\n'
body += FOOT
return body
def build_swift_page():
"""Build the /swift-codes/ search page."""
title = 'SWIFT/BIC Code Lookup — Find Bank BIC Codes Worldwide'
desc = 'Free SWIFT/BIC code lookup. Search by bank name, country, or BIC code. Database of major banks worldwide. All searches are client-side and private.'
extra = schema_script({
'@context': 'https://schema.org',
'@type': 'WebApplication',
'name': 'SWIFT/BIC Code Lookup',
'url': SITE + '/swift-codes/',
'description': 'Free SWIFT/BIC code lookup for banks worldwide',
'applicationCategory': 'FinanceApplication'
}) + schema_script(breadcrumb_schema([
('Home', SITE + '/'),
('SWIFT/BIC Codes', SITE + '/swift-codes/'),
]))
body = HEAD.format(
title=esc(title), desc=esc(desc),
canon=SITE + '/swift-codes/',
og_title=esc(title), og_desc=esc(desc),
extra=extra
)
body += '<nav class="breadcrumbs" aria-label="Breadcrumb"><a href="/">Home</a><span class="crumb-sep">/</span><span class="crumb-current">SWIFT/BIC Codes</span></nav>\n'
body += '<main class="main-content prose-content">\n'
body += '<h1>SWIFT/BIC Code Lookup</h1>\n'
body += '<p>Search for bank SWIFT/BIC codes worldwide. A BIC (Bank Identifier Code) identifies banks for international wire transfers. Search by bank name, country, or BIC code. All searches are private and run locally in your browser.</p>\n'
# Search UI
body += '<div class="swift-search-wrap">\n'
body += '<input type="text" class="swift-search-input" id="swift-search" placeholder="Search bank name, BIC, country, or city..." oninput="SwiftSearch.filter()" aria-label="Search SWIFT/BIC codes">\n'