-
-
Notifications
You must be signed in to change notification settings - Fork 113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support meta tags in StaticHTMLRenderer #483
Conversation
There was previously some discussion in #269 around designing a way to modify the head through a Scene similar to the struct MyApp : App {
var body: some Scene {
Head {
Meta(…)
}
WindowGroup(…) {
ContentView()
}
}
} |
@AndrewBarba would do you think about the suggested |
I really like that API, that is certainly my preference. It's very similar to Next.js which I think is a good thing. The static render defines that base html string which I thought was odd but I just tried to fit that. I would much prefer if Scene actually rendered that surrounding html/head/body structure. |
Yeah, that would require bigger changes to the renderer though. Let me know if you'd like to implement them yourself or have any questions. I can pick this up next week otherwise. |
I'm happy to do it in this PR I just need some direction (I don't know the codebase too well yet). If we keep the html string, what's the approach to pull out meta tags from the passed in view and render them in the head block? Also happy to pass this off to you if you already know obvious approach here. |
The hardcoded HTML string is just a hack. I'd suggest replacing the static HTML string with a tree of For that you'd initialize You could start with that, and introduce Let me know if any of this makes sense, I'm happy to provide more guidance if needed. |
This kind of makes sense. I would expect a new initializer that takes in a Scene which defines the entire HTML structure - including Meta tags and titles? And then the existing init that just takes in a View would wrap that View with this new base HTML structure. I have some confusion around |
No need for a new initializer, I don't think one would pass
Sorry about the confusion. |
@MaxDesiatov @carson-katri Check it out, I refactored PR to use preference keys so now you can drop var body: some View {
VStack {
...
Title("Hello, Tokamak")
Meta(charset: "utf-8")
...
}
} You can see the result working here: https://tokamak.app.swift.cloud |
Before I look into code just wanted to propose renaming I know the original issue had the short names, but I wonder if we should prepend |
I agree, I think prefixing makes more sense. I made this change and updated the PR description to show the correct usage. I added usage for both views and view modifiers. SwiftUI on iOS uses a lot of modifiers, ex. |
@@ -58,6 +58,12 @@ public final class StackReconciler<R: Renderer> { | |||
*/ | |||
public let rootTarget: R.TargetType | |||
|
|||
/** A root renderer's main preference store |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: comment formatting
/** A root renderer's main preference store | |
/** A root renderer's main preference store. |
|
||
import TokamakCore | ||
|
||
internal struct HTMLTitlePreferenceKey: PreferenceKey { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use internal
in our codebase as it's the default visibility and is redundant.
internal struct HTMLTitlePreferenceKey: PreferenceKey { | |
struct HTMLTitlePreferenceKey: PreferenceKey { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few nits to pick, but overall this makes sense. One more question: how does this behave if there are multiple Title
views in the same tree? Whichever comes last is applied I guess? Would you be able to add a snapshot test that confirms this? Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall it looks good to me, just needs a couple tests. I think they'd go in TokamakStaticHTMLTests/HTMLTests.swift
Could you add a test for |
Definitely - working on them now |
I pushed a test that uses 4 meta tags but only the last one ends up in the resulting html. Currently trying to debug this, I'm pretty sure my implementation is correct so I wonder if there's an issue with the preference store. |
Okay the main issue here is that every mount target gets its own preference store and then we are trying to merge upwards anytime a child store changes. This is problematic because the merge only saves the value of the child and doesn't maintain any values in the parent for the same key. I was able to fix this by:
I can push this up if you think it makes sense, otherwise I'm open to other approaches. public class MountedTarget {
var preferenceStore: _PreferenceStore
...
init(_ app: _AnyApp, _ environmentValues: EnvironmentValues, _ parent: MountedElement<R>?) {
element = .app(app)
self.parent = parent
self.preferenceStore = parent?.preferenceStore ?? .init()
self.environmentValues = environmentValues
viewTraits = .init()
updateEnvironment()
}
} |
Thanks for the investigation! IIRC @carson-katri is the author of preference store code, so I'll defer to him on this matter if he has a moment to review this. |
So does this make the child take all the values of the parent's preference store? If so, I don't think this behavior is correct as preference values flow up the tree not down. Similar to how an environment value from a child is not set on the parent, a preference value from a parent is not set on the child. |
Ah yes you are right. Don't think we have tests for this so I will add them. So I think the approach to solve that is add a weak parent store to the _PreferenceStore. Each mount target will go back to getting its own store but the pref store will be updated to modify its parent store anytime one of its keys changes. This is still a better solution to merge, because the issue with merge is it doesn't know which keys changed, and so even with some fancy generic work to deep merge keys upwards, it was over adding values to the parent store because it was doing a full merge on ever didSet. If we control the upstream changes to only happen on inserting a key this should be more reliable. |
…ee preference keys they should not have access to
@carson-katri I pushed that up, take a look let me know what you think. Im working on a test to cover that case now |
Okay just pushed a test that I think correctly covers what you described. |
….MetaTag public with better initializer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks good to me, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall this is great, one small change request: could you run SwiftFormat, and potentially have a pre-commit hook installed as described in the "Coding Style" section of CONTRIBUTING.md
? Some of the newly added files use 4 spaces for indentation, while we use 2 spaces everywhere in the project.
Thanks!
Yes definitely. Xcode is so annoying with the indents, I thought I caught them all. Will get this fixed |
Don't worry about catching them in Xcode manually. When I edit in Xcode I just write some messy inconsistent indentation, and it's all reformatted with pre-commit on every commit automatically 🙂 |
Yeah wow - this is so much better. My bad for not reading that code style doc earlier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lovely, thanks!
This PR adds the ability to control
<head>
tags using a newHTMLTitle
view andHTMLMeta
view. Taking inspiration from Next.js<NextHead>
, you can use these views anywhere in your view hierarchy and they will be hoisted to the top<head>
section of the html.Use as a view:
Use as a view modifier:
And the resulting html (no matter where these are used in your view hierarchy):