]> BookStack Code Mirror - api-scripts/blob - php-generate-sitemap/generate-sitemap.php
docs: add jaypyles/obsidian_to_bookstack
[api-scripts] / php-generate-sitemap / generate-sitemap.php
1 #!/usr/bin/env php
2 <?php
3
4 // API Credentials
5 // You can either provide them as environment variables
6 // or hard-code them in the empty strings below.
7 $baseUrl = getenv('BS_URL') ?: '';
8 $clientId = getenv('BS_TOKEN_ID') ?: '';
9 $clientSecret = getenv('BS_TOKEN_SECRET') ?: '';
10
11 // Output File
12 // Can be provided as a arguments when calling the script
13 // or be hard-coded as strings below.
14 $outputFile = $argv[1] ?? './sitemap.xml';
15
16 // Script logic
17 ////////////////
18
19 // Check we have required options
20 if (empty($outputFile)) {
21     errorOut("An output file needs to be provided");
22 }
23
24 // Create the output folder if it does not exist
25 $outDir = dirname($outputFile);
26 if (!is_dir($outDir)) {
27     mkdir($outDir, 0777, true);
28 }
29
30 // Clean up the base path
31 $baseUrl = rtrim($baseUrl, '/');
32
33 // Additional endpoints not fetched via API entities
34 $nowDate = date_format(new DateTime(), 'Y-m-d');
35 $additionalEndpoints = [
36     ['endpoint' => '/', 'updated' => $nowDate],
37     ['endpoint' => '/books', 'updated' => $nowDate],
38     ['endpoint' => '/search', 'updated' => $nowDate],
39     ['endpoint' => '/login', 'updated' => $nowDate],
40 ];
41
42 // Get all shelf URLs
43 $shelves = getAllOfAtListEndpoint("api/shelves", []);
44 $shelfEndpoints = array_map(function ($shelf) {
45     return ['endpoint' => '/shelves/' . $shelf['slug'], 'updated' => $shelf['updated_at']];
46 }, $shelves);
47
48 // Get all book URLs and map for chapters & pages
49 $books = getAllOfAtListEndpoint("api/books", []);
50 $bookSlugsById = [];
51 $bookEndpoints = array_map(function ($book) use (&$bookSlugsById) {
52     $bookSlugsById[$book['id']] = $book['slug'];
53     return ['endpoint' => '/books/' . $book['slug'], 'updated' => $book['updated_at']];
54 }, $books);
55
56 // Get all chapter URLs and map for pages
57 $chapters = getAllOfAtListEndpoint("api/chapters", []);
58 $chapterEndpoints = array_map(function ($chapter) use ($bookSlugsById) {
59     $bookSlug = $bookSlugsById[$chapter['book_id']];
60     return ['endpoint' => '/books/' . $bookSlug . '/chapter/' . $chapter['slug'], 'updated' => $chapter['updated_at']];
61 }, $chapters);
62
63 // Get all page URLs
64 $pages = getAllOfAtListEndpoint("api/pages", []);
65 $pageEndpoints = array_map(function ($page) use ($bookSlugsById) {
66     $bookSlug = $bookSlugsById[$page['book_id']];
67     return ['endpoint' => '/books/' . $bookSlug . '/page/' . $page['slug'], 'updated' => $page['updated_at']];
68 }, $pages);
69
70 // Gather all our endpoints
71 $allEndpoints = array_merge(
72     $additionalEndpoints,
73     $pageEndpoints,
74     $chapterEndpoints,
75     $bookEndpoints,
76     $shelfEndpoints
77 );
78
79 // Fetch our sitemap XML
80 $xmlSitemap = generateSitemapXml($allEndpoints);
81 // Write to the output file
82 file_put_contents($outputFile, $xmlSitemap);
83
84 /**
85  * Generate out the XML content for a sitemap
86  * for the given URLs.
87  */
88 function generateSitemapXml(array $endpoints): string
89 {
90     global $baseUrl;
91     $doc = new DOMDocument("1.0", "UTF-8");
92     $doc->formatOutput = true;
93     $urlset = $doc->createElement('urlset');
94     $urlset->setAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
95
96     $doc->appendChild($urlset);
97     foreach ($endpoints as $endpointInfo) {
98         $date = (new DateTime($endpointInfo['updated']))->format('Y-m-d');
99         $url = $doc->createElement('url');
100         $loc = $url->appendChild($doc->createElement('loc'));
101         $urlText = $doc->createTextNode($baseUrl . $endpointInfo['endpoint']);
102         $loc->appendChild($urlText);
103         $url->appendChild($doc->createElement('lastmod', $date));
104         $url->appendChild($doc->createElement('changefreq', 'monthly'));
105         $url->appendChild($doc->createElement('priority', '0.8'));
106         $urlset->appendChild($url);
107     }
108
109     return $doc->saveXML();
110 }
111
112 /**
113  * Consume all items from the given API listing endpoint.
114  */
115 function getAllOfAtListEndpoint(string $endpoint, array $params): array
116 {
117     $count = 100;
118     $offset = 0;
119     $all = [];
120
121     do {
122         $endpoint = $endpoint . '?' . http_build_query(array_merge($params, ['count' => $count, 'offset' => $offset]));
123         $resp = apiGetJson($endpoint);
124
125         $total = $resp['total'] ?? 0;
126         $new = $resp['data'] ?? [];
127         array_push($all, ...$new);
128         $offset += $count;
129     } while ($offset < $total);
130
131     return $all;
132 }
133
134 /**
135  * Make a simple GET HTTP request to the API.
136  */
137 function apiGet(string $endpoint): string
138 {
139     global $baseUrl, $clientId, $clientSecret;
140     $url = rtrim($baseUrl, '/') . '/' . ltrim($endpoint, '/');
141     $opts = ['http' => ['header' => "Authorization: Token {$clientId}:{$clientSecret}"]];
142     $context = stream_context_create($opts);
143     return @file_get_contents($url, false, $context);
144 }
145
146 /**
147  * Make a simple GET HTTP request to the API &
148  * decode the JSON response to an array.
149  */
150 function apiGetJson(string $endpoint): array
151 {
152     $data = apiGet($endpoint);
153     return json_decode($data, true);
154 }
155
156 /**
157  * DEBUG: Dump out the given variables and exit.
158  */
159 function dd(...$args)
160 {
161     foreach ($args as $arg) {
162         var_dump($arg);
163     }
164     exit(1);
165 }
166
167 /**
168  * Alert of an error then exit the script.
169  */
170 function errorOut(string $text)
171 {
172     echo "ERROR: " . $text;
173     exit(1);
174 }