elm-pages 2.1.7 → 2.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/generator/review/elm.json +34 -0
- package/generator/review/src/ReviewConfig.elm +10 -0
- package/generator/src/basepath-middleware.js +15 -9
- package/generator/src/build.js +77 -4
- package/generator/src/cli.js +13 -9
- package/generator/src/compile-elm.js +43 -0
- package/generator/src/dev-server.js +63 -11
- package/generator/src/error-formatter.js +62 -9
- package/generator/src/generate-template-module-connector.js +17 -4
- package/generator/src/init.js +4 -0
- package/generator/src/pre-render-html.js +19 -12
- package/generator/src/render-worker.js +0 -1
- package/generator/src/render.js +1 -2
- package/generator/src/seo-renderer.js +21 -2
- package/generator/static-code/hmr.js +43 -6
- package/generator/template/elm.json +13 -5
- package/generator/template/package.json +3 -2
- package/package.json +14 -8
- package/src/ApiRoute.elm +178 -0
- package/src/AriaLiveAnnouncer.elm +36 -0
- package/src/BuildError.elm +60 -0
- package/src/DataSource/File.elm +288 -0
- package/src/DataSource/Glob.elm +1050 -0
- package/src/DataSource/Http.elm +467 -0
- package/src/DataSource/Internal/Glob.elm +74 -0
- package/src/DataSource/Port.elm +87 -0
- package/src/DataSource/ServerRequest.elm +60 -0
- package/src/DataSource.elm +801 -0
- package/src/Head/Seo.elm +516 -0
- package/src/Head/Twitter.elm +109 -0
- package/src/Head.elm +452 -0
- package/src/HtmlPrinter.elm +27 -0
- package/src/Internal/ApiRoute.elm +89 -0
- package/src/Internal/OptimizedDecoder.elm +18 -0
- package/src/KeepOrDiscard.elm +6 -0
- package/src/OptimizedDecoder/Pipeline.elm +335 -0
- package/src/OptimizedDecoder.elm +818 -0
- package/src/Pages/ContentCache.elm +248 -0
- package/src/Pages/Flags.elm +26 -0
- package/src/Pages/Http.elm +10 -0
- package/src/Pages/Internal/ApplicationType.elm +6 -0
- package/src/Pages/Internal/NotFoundReason.elm +256 -0
- package/src/Pages/Internal/Platform/Cli.elm +1015 -0
- package/src/Pages/Internal/Platform/Effect.elm +14 -0
- package/src/Pages/Internal/Platform/StaticResponses.elm +540 -0
- package/src/Pages/Internal/Platform/ToJsPayload.elm +138 -0
- package/src/Pages/Internal/Platform.elm +745 -0
- package/src/Pages/Internal/RoutePattern.elm +122 -0
- package/src/Pages/Internal/Router.elm +116 -0
- package/src/Pages/Internal/StaticHttpBody.elm +54 -0
- package/src/Pages/Internal/String.elm +39 -0
- package/src/Pages/Manifest/Category.elm +240 -0
- package/src/Pages/Manifest.elm +412 -0
- package/src/Pages/PageUrl.elm +38 -0
- package/src/Pages/ProgramConfig.elm +73 -0
- package/src/Pages/Review/NoContractViolations.elm +397 -0
- package/src/Pages/Secrets.elm +83 -0
- package/src/Pages/SiteConfig.elm +13 -0
- package/src/Pages/StaticHttp/Request.elm +42 -0
- package/src/Pages/StaticHttpRequest.elm +320 -0
- package/src/Pages/Url.elm +60 -0
- package/src/Path.elm +96 -0
- package/src/QueryParams.elm +216 -0
- package/src/RenderRequest.elm +163 -0
- package/src/RequestsAndPending.elm +20 -0
- package/src/Secrets.elm +111 -0
- package/src/SecretsDict.elm +45 -0
- package/src/StructuredData.elm +236 -0
- package/src/TerminalText.elm +242 -0
- package/src/Test/Html/Internal/ElmHtml/Constants.elm +53 -0
- package/src/Test/Html/Internal/ElmHtml/Helpers.elm +17 -0
- package/src/Test/Html/Internal/ElmHtml/InternalTypes.elm +529 -0
- package/src/Test/Html/Internal/ElmHtml/Markdown.elm +56 -0
- package/src/Test/Html/Internal/ElmHtml/ToString.elm +197 -0
- package/src/Test/Internal/KernelConstants.elm +34 -0
package/src/Head/Seo.elm
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
module Head.Seo exposing (Common, Image, article, audioPlayer, book, profile, song, summary, summaryLarge, videoPlayer, website)
|
|
2
|
+
|
|
3
|
+
{-| <https://ogp.me/#>
|
|
4
|
+
<https://developers.facebook.com/docs/sharing/opengraph>
|
|
5
|
+
|
|
6
|
+
This module encapsulates some of the best practices for SEO for your site.
|
|
7
|
+
|
|
8
|
+
`elm-pages` will pre-render each of the static pages (in your `content` directory) so that
|
|
9
|
+
web crawlers can efficiently and accurately process it. The functions in this module are for use
|
|
10
|
+
with the `head` function that you pass to your Pages config (`Pages.application`).
|
|
11
|
+
|
|
12
|
+
import Date
|
|
13
|
+
import Head
|
|
14
|
+
import Head.Seo as Seo
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
-- justinmimbs/date package
|
|
18
|
+
type alias ArticleMetadata =
|
|
19
|
+
{ title : String
|
|
20
|
+
, description : String
|
|
21
|
+
, published : Date
|
|
22
|
+
, author : Data.Author.Author
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
head : ArticleMetadata -> List Head.Tag
|
|
26
|
+
head articleMetadata =
|
|
27
|
+
Seo.summaryLarge
|
|
28
|
+
{ canonicalUrlOverride = Nothing
|
|
29
|
+
, siteName = "elm-pages"
|
|
30
|
+
, image =
|
|
31
|
+
{ url = Pages.images.icon
|
|
32
|
+
, alt = articleMetadata.description
|
|
33
|
+
, dimensions = Nothing
|
|
34
|
+
, mimeType = Nothing
|
|
35
|
+
}
|
|
36
|
+
, description = articleMetadata.description
|
|
37
|
+
, locale = Nothing
|
|
38
|
+
, title = articleMetadata.title
|
|
39
|
+
}
|
|
40
|
+
|> Seo.article
|
|
41
|
+
{ tags = []
|
|
42
|
+
, section = Nothing
|
|
43
|
+
, publishedTime = Just (Date.toIsoString articleMetadata.published)
|
|
44
|
+
, modifiedTime = Nothing
|
|
45
|
+
, expirationTime = Nothing
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@docs Common, Image, article, audioPlayer, book, profile, song, summary, summaryLarge, videoPlayer, website
|
|
49
|
+
|
|
50
|
+
-}
|
|
51
|
+
|
|
52
|
+
import Head
|
|
53
|
+
import Head.Twitter as Twitter
|
|
54
|
+
import Pages.Url
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
{-| Will be displayed as a large card in twitter
|
|
58
|
+
See: <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/summary-card-with-large-image>
|
|
59
|
+
|
|
60
|
+
The options will also be used to build up the appropriate OpenGraph `<meta>` tags.
|
|
61
|
+
|
|
62
|
+
Note: You cannot include audio or video tags with summaries.
|
|
63
|
+
If you want one of those, use `audioPlayer` or `videoPlayer`
|
|
64
|
+
|
|
65
|
+
-}
|
|
66
|
+
summaryLarge :
|
|
67
|
+
{ canonicalUrlOverride : Maybe String
|
|
68
|
+
, siteName : String
|
|
69
|
+
, image : Image
|
|
70
|
+
, description : String
|
|
71
|
+
, title : String
|
|
72
|
+
, locale : Maybe Locale
|
|
73
|
+
}
|
|
74
|
+
-> Common
|
|
75
|
+
summaryLarge config =
|
|
76
|
+
buildSummary config Twitter.Large
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
{-| Will be displayed as a large card in twitter
|
|
80
|
+
See: <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/summary>
|
|
81
|
+
|
|
82
|
+
The options will also be used to build up the appropriate OpenGraph `<meta>` tags.
|
|
83
|
+
|
|
84
|
+
Note: You cannot include audio or video tags with summaries.
|
|
85
|
+
If you want one of those, use `audioPlayer` or `videoPlayer`
|
|
86
|
+
|
|
87
|
+
-}
|
|
88
|
+
summary :
|
|
89
|
+
{ canonicalUrlOverride : Maybe String
|
|
90
|
+
, siteName : String
|
|
91
|
+
, image : Image
|
|
92
|
+
, description : String
|
|
93
|
+
, title : String
|
|
94
|
+
, locale : Maybe Locale
|
|
95
|
+
}
|
|
96
|
+
-> Common
|
|
97
|
+
summary config =
|
|
98
|
+
buildSummary config Twitter.Regular
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
{-| Will be displayed as a Player card in twitter
|
|
102
|
+
See: <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/player-card>
|
|
103
|
+
|
|
104
|
+
OpenGraph audio will also be included.
|
|
105
|
+
The options will also be used to build up the appropriate OpenGraph `<meta>` tags.
|
|
106
|
+
|
|
107
|
+
-}
|
|
108
|
+
audioPlayer :
|
|
109
|
+
{ canonicalUrlOverride : Maybe String
|
|
110
|
+
, siteName : String
|
|
111
|
+
, image : Image
|
|
112
|
+
, description : String
|
|
113
|
+
, title : String
|
|
114
|
+
, audio : Audio
|
|
115
|
+
, locale : Maybe Locale
|
|
116
|
+
}
|
|
117
|
+
-> Common
|
|
118
|
+
audioPlayer { title, image, canonicalUrlOverride, description, siteName, audio, locale } =
|
|
119
|
+
{ title = title
|
|
120
|
+
, image = image
|
|
121
|
+
, canonicalUrlOverride = canonicalUrlOverride
|
|
122
|
+
, description = description
|
|
123
|
+
, siteName = siteName
|
|
124
|
+
, audio = Just audio
|
|
125
|
+
, video = Nothing
|
|
126
|
+
, locale = locale
|
|
127
|
+
, alternateLocales = [] -- TODO remove hardcoding
|
|
128
|
+
, twitterCard =
|
|
129
|
+
Twitter.Player
|
|
130
|
+
{ title = title
|
|
131
|
+
, description = Just description
|
|
132
|
+
, siteUser = ""
|
|
133
|
+
, image = { url = image.url, alt = image.alt }
|
|
134
|
+
, player = audio.url
|
|
135
|
+
|
|
136
|
+
-- TODO what should I do here? These are requried by Twitter...
|
|
137
|
+
-- probably require them for both (strictest common requirement)
|
|
138
|
+
, width = 0
|
|
139
|
+
, height = 0
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
{-| Will be displayed as a Player card in twitter
|
|
145
|
+
See: <https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/player-card>
|
|
146
|
+
|
|
147
|
+
OpenGraph video will also be included.
|
|
148
|
+
The options will also be used to build up the appropriate OpenGraph `<meta>` tags.
|
|
149
|
+
|
|
150
|
+
-}
|
|
151
|
+
videoPlayer :
|
|
152
|
+
{ canonicalUrlOverride : Maybe String
|
|
153
|
+
, siteName : String
|
|
154
|
+
, image : Image
|
|
155
|
+
, description : String
|
|
156
|
+
, title : String
|
|
157
|
+
, video : Video
|
|
158
|
+
, locale : Maybe Locale
|
|
159
|
+
}
|
|
160
|
+
-> Common
|
|
161
|
+
videoPlayer { title, image, canonicalUrlOverride, description, siteName, video, locale } =
|
|
162
|
+
{ title = title
|
|
163
|
+
, image = image
|
|
164
|
+
, canonicalUrlOverride = canonicalUrlOverride
|
|
165
|
+
, description = description
|
|
166
|
+
, siteName = siteName
|
|
167
|
+
, audio = Nothing
|
|
168
|
+
, video = Just video
|
|
169
|
+
, locale = locale
|
|
170
|
+
, alternateLocales = [] -- TODO remove hardcoding
|
|
171
|
+
, twitterCard =
|
|
172
|
+
Twitter.Player
|
|
173
|
+
{ title = title
|
|
174
|
+
, description = Just description
|
|
175
|
+
, siteUser = ""
|
|
176
|
+
, image = { url = image.url, alt = image.alt }
|
|
177
|
+
, player = video.url
|
|
178
|
+
|
|
179
|
+
-- TODO what should I do here? These are requried by Twitter...
|
|
180
|
+
-- probably require them for both (strictest common requirement)
|
|
181
|
+
, width = 0
|
|
182
|
+
, height = 0
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
buildSummary :
|
|
188
|
+
{ canonicalUrlOverride : Maybe String
|
|
189
|
+
, siteName : String
|
|
190
|
+
, image : Image
|
|
191
|
+
, description : String
|
|
192
|
+
, title : String
|
|
193
|
+
, locale : Maybe Locale
|
|
194
|
+
}
|
|
195
|
+
-> Twitter.SummarySize
|
|
196
|
+
-> Common
|
|
197
|
+
buildSummary { title, image, canonicalUrlOverride, description, siteName, locale } summarySize =
|
|
198
|
+
{ title = title
|
|
199
|
+
, image = image
|
|
200
|
+
, canonicalUrlOverride = canonicalUrlOverride
|
|
201
|
+
, description = description
|
|
202
|
+
, siteName = siteName
|
|
203
|
+
, audio = Nothing
|
|
204
|
+
, video = Nothing
|
|
205
|
+
, locale = locale
|
|
206
|
+
, alternateLocales = [] -- TODO remove hardcoding
|
|
207
|
+
, twitterCard =
|
|
208
|
+
Twitter.Summary
|
|
209
|
+
{ title = title
|
|
210
|
+
, description = Just description
|
|
211
|
+
, siteUser = Nothing -- TODO remove hardcoding
|
|
212
|
+
, image = Just { url = image.url, alt = image.alt }
|
|
213
|
+
, size = summarySize
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
-- TODO add constructor Twitter app-card
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
{-| <https://ogp.me/#type_website>
|
|
223
|
+
-}
|
|
224
|
+
website :
|
|
225
|
+
Common
|
|
226
|
+
-> List Head.Tag
|
|
227
|
+
website common =
|
|
228
|
+
Website |> Content common |> tags
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
{-| See <https://ogp.me/#type_article>
|
|
232
|
+
-}
|
|
233
|
+
article :
|
|
234
|
+
{ tags : List String
|
|
235
|
+
, section : Maybe String
|
|
236
|
+
, publishedTime : Maybe Iso8601DateTime
|
|
237
|
+
, modifiedTime : Maybe Iso8601DateTime
|
|
238
|
+
, expirationTime : Maybe Iso8601DateTime
|
|
239
|
+
}
|
|
240
|
+
-> Common
|
|
241
|
+
-> List Head.Tag
|
|
242
|
+
article details common =
|
|
243
|
+
Article details |> Content common |> tags
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
{-| See <https://ogp.me/#type_book>
|
|
247
|
+
-}
|
|
248
|
+
book :
|
|
249
|
+
Common
|
|
250
|
+
->
|
|
251
|
+
{ tags : List String
|
|
252
|
+
, isbn : Maybe String
|
|
253
|
+
, releaseDate : Maybe Iso8601DateTime
|
|
254
|
+
}
|
|
255
|
+
-> List Head.Tag
|
|
256
|
+
book common details =
|
|
257
|
+
Book details |> Content common |> tags
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
{-| See <https://ogp.me/#type_profile>
|
|
261
|
+
-}
|
|
262
|
+
profile :
|
|
263
|
+
{ firstName : String
|
|
264
|
+
, lastName : String
|
|
265
|
+
, username : Maybe String
|
|
266
|
+
}
|
|
267
|
+
-> Common
|
|
268
|
+
-> List Head.Tag
|
|
269
|
+
profile details common =
|
|
270
|
+
Profile details |> Content common |> tags
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
{-| See <https://ogp.me/#type_music.song>
|
|
274
|
+
-}
|
|
275
|
+
song :
|
|
276
|
+
Common
|
|
277
|
+
->
|
|
278
|
+
{ duration : Maybe Int
|
|
279
|
+
, album : Maybe Int
|
|
280
|
+
, disc : Maybe Int
|
|
281
|
+
, track : Maybe Int
|
|
282
|
+
}
|
|
283
|
+
-> List Head.Tag
|
|
284
|
+
song common details =
|
|
285
|
+
Song details |> Content common |> tags
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
{-| These fields apply to any type in the og object types
|
|
289
|
+
See <https://ogp.me/#metadata> and <https://ogp.me/#optional>
|
|
290
|
+
|
|
291
|
+
Skipping this for now, if there's a use case I can add it in:
|
|
292
|
+
|
|
293
|
+
- og:determiner - The word that appears before this object's title in a sentence. An enum of (a, an, the, "", auto). If auto is chosen, the consumer of your data should chose between "a" or "an". Default is "" (blank).
|
|
294
|
+
|
|
295
|
+
-}
|
|
296
|
+
type alias Common =
|
|
297
|
+
{ title : String
|
|
298
|
+
, image : Image
|
|
299
|
+
, canonicalUrlOverride : Maybe String
|
|
300
|
+
, description : String
|
|
301
|
+
, siteName : String
|
|
302
|
+
, audio : Maybe Audio
|
|
303
|
+
, video : Maybe Video
|
|
304
|
+
, locale : Maybe Locale
|
|
305
|
+
, alternateLocales : List Locale
|
|
306
|
+
, twitterCard : Twitter.TwitterCard
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
tagsForCommon : Common -> List ( String, Maybe Head.AttributeValue )
|
|
311
|
+
tagsForCommon common =
|
|
312
|
+
tagsForImage common.image
|
|
313
|
+
++ (common.audio |> Maybe.map tagsForAudio |> Maybe.withDefault [])
|
|
314
|
+
++ (common.video |> Maybe.map tagsForVideo |> Maybe.withDefault [])
|
|
315
|
+
++ [ ( "og:title", Just (Head.raw common.title) )
|
|
316
|
+
, ( "og:url", common.canonicalUrlOverride |> Maybe.map Head.raw |> Maybe.withDefault Head.currentPageFullUrl |> Just )
|
|
317
|
+
, ( "og:description", Just (Head.raw common.description) )
|
|
318
|
+
, ( "og:site_name", Just (Head.raw common.siteName) )
|
|
319
|
+
, ( "og:locale", common.locale |> Maybe.map Head.raw )
|
|
320
|
+
]
|
|
321
|
+
++ (common.alternateLocales
|
|
322
|
+
|> List.map
|
|
323
|
+
(\alternateLocale ->
|
|
324
|
+
( "og:locale:alternate", alternateLocale |> Head.raw |> Just )
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
++ Twitter.rawTags common.twitterCard
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
{-| See the audio section in <https://ogp.me/#structured>
|
|
331
|
+
Example:
|
|
332
|
+
|
|
333
|
+
{ url = "https://example.com/sound.mp3"
|
|
334
|
+
mimeType = Just "audio/mpeg"
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
-}
|
|
338
|
+
type alias Audio =
|
|
339
|
+
{ url : String
|
|
340
|
+
, mimeType : Maybe MimeType
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
tagsForAudio : Audio -> List ( String, Maybe Head.AttributeValue )
|
|
345
|
+
tagsForAudio audio =
|
|
346
|
+
[ ( "og:audio", audio.url |> Head.raw |> Just )
|
|
347
|
+
, ( "og:audio:secure_url", audio.url |> Head.raw |> Just )
|
|
348
|
+
, ( "og:audio:type", audio.mimeType |> Maybe.map Head.raw )
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
type alias Locale =
|
|
353
|
+
-- TODO make this more type-safe
|
|
354
|
+
String
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
type Content
|
|
358
|
+
= Content Common ContentDetails
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
type ContentDetails
|
|
362
|
+
= Website
|
|
363
|
+
| Article
|
|
364
|
+
{ tags : List String
|
|
365
|
+
, section : Maybe String
|
|
366
|
+
, publishedTime : Maybe Iso8601DateTime
|
|
367
|
+
, modifiedTime : Maybe Iso8601DateTime
|
|
368
|
+
, expirationTime : Maybe Iso8601DateTime
|
|
369
|
+
}
|
|
370
|
+
| Book
|
|
371
|
+
{ tags : List String
|
|
372
|
+
, isbn : Maybe String
|
|
373
|
+
, releaseDate : Maybe Iso8601DateTime
|
|
374
|
+
}
|
|
375
|
+
| Song
|
|
376
|
+
{-
|
|
377
|
+
|
|
378
|
+
TODO
|
|
379
|
+
music:album - music.album array - The album this song is from.
|
|
380
|
+
music:musician - profile array - The musician that made this song.
|
|
381
|
+
-}
|
|
382
|
+
{ duration : Maybe Int
|
|
383
|
+
, album : Maybe Int
|
|
384
|
+
, disc : Maybe Int
|
|
385
|
+
, track : Maybe Int
|
|
386
|
+
}
|
|
387
|
+
| Profile
|
|
388
|
+
{ firstName : String
|
|
389
|
+
, lastName : String
|
|
390
|
+
, username : Maybe String
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
{-| <https://en.wikipedia.org/wiki/ISO_8601>
|
|
395
|
+
-}
|
|
396
|
+
type alias Iso8601DateTime =
|
|
397
|
+
-- TODO should be more type-safe here
|
|
398
|
+
String
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
{-| <https://en.wikipedia.org/wiki/Media_type>
|
|
402
|
+
-}
|
|
403
|
+
type alias MimeType =
|
|
404
|
+
-- TODO should be more type-safe here
|
|
405
|
+
String
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
{-| See <https://ogp.me/#structured>
|
|
409
|
+
-}
|
|
410
|
+
type alias Image =
|
|
411
|
+
{ url : Pages.Url.Url
|
|
412
|
+
, alt : String
|
|
413
|
+
, dimensions : Maybe { width : Int, height : Int }
|
|
414
|
+
, mimeType : Maybe MimeType
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
tagsForImage : Image -> List ( String, Maybe Head.AttributeValue )
|
|
419
|
+
tagsForImage image =
|
|
420
|
+
[ ( "og:image", Just (Head.urlAttribute image.url) )
|
|
421
|
+
, ( "og:image:secure_url", Just (Head.urlAttribute image.url) )
|
|
422
|
+
, ( "og:image:alt", image.alt |> Head.raw |> Just )
|
|
423
|
+
, ( "og:image:width", image.dimensions |> Maybe.map .width |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
424
|
+
, ( "og:image:height", image.dimensions |> Maybe.map .height |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
{-| See <https://ogp.me/#structured>
|
|
429
|
+
-}
|
|
430
|
+
type alias Video =
|
|
431
|
+
{ url : String
|
|
432
|
+
, mimeType : Maybe MimeType
|
|
433
|
+
, dimensions : Maybe { width : Int, height : Int }
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
tagsForVideo : Video -> List ( String, Maybe Head.AttributeValue )
|
|
438
|
+
tagsForVideo video =
|
|
439
|
+
[ ( "og:video", video.url |> Head.raw |> Just )
|
|
440
|
+
, ( "og:video:secure_url", video.url |> Head.raw |> Just )
|
|
441
|
+
, ( "og:video:width", video.dimensions |> Maybe.map .width |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
442
|
+
, ( "og:video:height", video.dimensions |> Maybe.map .height |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
tags : Content -> List Head.Tag
|
|
447
|
+
tags (Content common details) =
|
|
448
|
+
tagsForCommon common
|
|
449
|
+
++ (case details of
|
|
450
|
+
Website ->
|
|
451
|
+
[ ( "og:type", "website" |> Head.raw |> Just )
|
|
452
|
+
]
|
|
453
|
+
|
|
454
|
+
Article articleDetails ->
|
|
455
|
+
{-
|
|
456
|
+
TODO
|
|
457
|
+
- article:author - profile array - Writers of the article.
|
|
458
|
+
-}
|
|
459
|
+
[ ( "og:type", "article" |> Head.raw |> Just )
|
|
460
|
+
, ( "article:section", articleDetails.section |> Maybe.map Head.raw )
|
|
461
|
+
, ( "article:published_time", articleDetails.publishedTime |> Maybe.map Head.raw )
|
|
462
|
+
, ( "article:modified_time", articleDetails.modifiedTime |> Maybe.map Head.raw )
|
|
463
|
+
, ( "article:expiration_time", articleDetails.expirationTime |> Maybe.map Head.raw )
|
|
464
|
+
]
|
|
465
|
+
++ List.map
|
|
466
|
+
(\tag -> ( "article:tag", tag |> Head.raw |> Just ))
|
|
467
|
+
articleDetails.tags
|
|
468
|
+
|
|
469
|
+
Book bookDetails ->
|
|
470
|
+
[ ( "og:type", "book" |> Head.raw |> Just )
|
|
471
|
+
, ( "og:isbn", bookDetails.isbn |> Maybe.map Head.raw )
|
|
472
|
+
, ( "og:release_date", bookDetails.releaseDate |> Maybe.map Head.raw )
|
|
473
|
+
]
|
|
474
|
+
++ List.map
|
|
475
|
+
(\tag -> ( "book:tag", tag |> Head.raw |> Just ))
|
|
476
|
+
bookDetails.tags
|
|
477
|
+
|
|
478
|
+
Song songDetails ->
|
|
479
|
+
[ ( "og:type", "music.song" |> Head.raw |> Just )
|
|
480
|
+
, ( "music:duration", songDetails.duration |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
481
|
+
, ( "music:album:disc", songDetails.disc |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
482
|
+
, ( "music:album:track", songDetails.track |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
Profile profileDetails ->
|
|
486
|
+
[ ( "og:type", "profile" |> Head.raw |> Just )
|
|
487
|
+
, ( "profile:first_name", profileDetails.firstName |> Head.raw |> Just )
|
|
488
|
+
, ( "profile:last_name", profileDetails.lastName |> Head.raw |> Just )
|
|
489
|
+
, ( "profile:username", profileDetails.username |> Maybe.map Head.raw )
|
|
490
|
+
]
|
|
491
|
+
)
|
|
492
|
+
|> List.filterMap
|
|
493
|
+
(\( name, maybeContent ) ->
|
|
494
|
+
maybeContent
|
|
495
|
+
|> Maybe.map (\metaContent -> Head.metaProperty name metaContent)
|
|
496
|
+
)
|
|
497
|
+
|> List.append
|
|
498
|
+
[ Head.canonicalLink common.canonicalUrlOverride
|
|
499
|
+
, Head.metaName "description" (Head.raw common.description)
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
{-
|
|
505
|
+
TODO remaining types:
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
- music.album
|
|
509
|
+
- music.playlist
|
|
510
|
+
- music.radio_station
|
|
511
|
+
- video.movie
|
|
512
|
+
- video.episode
|
|
513
|
+
- video.tv_show
|
|
514
|
+
- video.other
|
|
515
|
+
|
|
516
|
+
-}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module Head.Twitter exposing (SummarySize(..), TwitterCard(..), rawTags)
|
|
2
|
+
|
|
3
|
+
import Head
|
|
4
|
+
import Pages.Url
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
type SummarySize
|
|
8
|
+
= Regular
|
|
9
|
+
| Large
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
type alias Image =
|
|
13
|
+
{ url : Pages.Url.Url
|
|
14
|
+
, alt : String
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
type TwitterCard
|
|
19
|
+
= Summary
|
|
20
|
+
{ title : String
|
|
21
|
+
, description : Maybe String
|
|
22
|
+
, siteUser : Maybe String
|
|
23
|
+
, image : Maybe Image
|
|
24
|
+
, size : SummarySize
|
|
25
|
+
}
|
|
26
|
+
| App
|
|
27
|
+
{ title : String
|
|
28
|
+
, description : Maybe String
|
|
29
|
+
, siteUser : String
|
|
30
|
+
, image : Maybe Image
|
|
31
|
+
, appIdIphone : Maybe Int
|
|
32
|
+
, appIdIpad : Maybe Int
|
|
33
|
+
, appIdGooglePlay : Maybe String
|
|
34
|
+
, appUrlIphone : Maybe String
|
|
35
|
+
, appUrlIpad : Maybe String
|
|
36
|
+
, appUrlGooglePlay : Maybe String
|
|
37
|
+
, appCountry : Maybe String
|
|
38
|
+
, appNameIphone : Maybe String
|
|
39
|
+
, appNameIpad : Maybe String
|
|
40
|
+
, appNameGooglePlay : Maybe String
|
|
41
|
+
}
|
|
42
|
+
-- https://developer.twitter.com/en/docs/tweets/optimize-with-cards/overview/player-card
|
|
43
|
+
| Player
|
|
44
|
+
{ title : String
|
|
45
|
+
, description : Maybe String
|
|
46
|
+
, siteUser : String
|
|
47
|
+
, image : Image
|
|
48
|
+
, player : String
|
|
49
|
+
, width : Int
|
|
50
|
+
, height : Int
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
rawTags : TwitterCard -> List ( String, Maybe Head.AttributeValue )
|
|
55
|
+
rawTags card =
|
|
56
|
+
( "twitter:card", cardValue card |> Head.raw |> Just )
|
|
57
|
+
:: (case card of
|
|
58
|
+
Summary details ->
|
|
59
|
+
[ ( "twitter:title", details.title |> Head.raw |> Just )
|
|
60
|
+
, ( "twitter:site", details.siteUser |> Maybe.map Head.raw )
|
|
61
|
+
, ( "twitter:description", details.description |> Maybe.map Head.raw )
|
|
62
|
+
, ( "twitter:image", details.image |> Maybe.map .url |> Maybe.map Head.urlAttribute )
|
|
63
|
+
, ( "twitter:image:alt", details.image |> Maybe.map .alt |> Maybe.map Head.raw )
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
App details ->
|
|
67
|
+
[ ( "twitter:title", details.title |> Head.raw |> Just )
|
|
68
|
+
, ( "twitter:site", details.siteUser |> Head.raw |> Just )
|
|
69
|
+
, ( "twitter:description", details.description |> Maybe.map Head.raw )
|
|
70
|
+
, ( "twitter:image", details.image |> Maybe.map .url |> Maybe.map Head.urlAttribute )
|
|
71
|
+
, ( "twitter:image:alt", details.image |> Maybe.map .alt |> Maybe.map Head.raw )
|
|
72
|
+
, ( "twitter:app:name:iphone", details.appNameIphone |> Maybe.map Head.raw )
|
|
73
|
+
, ( "twitter:app:name:ipad", details.appNameIpad |> Maybe.map Head.raw )
|
|
74
|
+
, ( "twitter:app:name:googleplay", details.appNameGooglePlay |> Maybe.map Head.raw )
|
|
75
|
+
, ( "twitter:app:id:iphone", details.appIdIphone |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
76
|
+
, ( "twitter:app:id:ipad", details.appIdIpad |> Maybe.map String.fromInt |> Maybe.map Head.raw )
|
|
77
|
+
, ( "twitter:app:id:googleplay", details.appIdGooglePlay |> Maybe.map Head.raw )
|
|
78
|
+
, ( "twitter:app:url:iphone", details.appUrlIphone |> Maybe.map Head.raw )
|
|
79
|
+
, ( "twitter:app:url:ipad", details.appUrlIpad |> Maybe.map Head.raw )
|
|
80
|
+
, ( "twitter:app:url:googleplay", details.appUrlGooglePlay |> Maybe.map Head.raw )
|
|
81
|
+
, ( "twitter:app:country", details.appCountry |> Maybe.map Head.raw )
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
Player details ->
|
|
85
|
+
[ ( "twitter:title", details.title |> Head.raw |> Just )
|
|
86
|
+
, ( "twitter:site", details.siteUser |> Head.raw |> Just )
|
|
87
|
+
, ( "twitter:description", details.description |> Maybe.map Head.raw )
|
|
88
|
+
, ( "twitter:image", Just (Head.urlAttribute details.image.url) )
|
|
89
|
+
, ( "twitter:image:alt", details.image.alt |> Head.raw |> Just )
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
cardValue : TwitterCard -> String
|
|
95
|
+
cardValue card =
|
|
96
|
+
case card of
|
|
97
|
+
Summary details ->
|
|
98
|
+
case details.size of
|
|
99
|
+
Regular ->
|
|
100
|
+
"summary"
|
|
101
|
+
|
|
102
|
+
Large ->
|
|
103
|
+
"summary_large_image"
|
|
104
|
+
|
|
105
|
+
App _ ->
|
|
106
|
+
"app"
|
|
107
|
+
|
|
108
|
+
Player _ ->
|
|
109
|
+
"player"
|